From da9756534b0424071dd1ef91217a99d7e980096c Mon Sep 17 00:00:00 2001 From: Peterjah Date: Tue, 17 Dec 2024 15:44:30 +0100 Subject: [PATCH 01/10] add global config file manager --- cmd/massa-wallet/massa-wallet.go | 3 ++ go.mod | 10 +++- go.sum | 20 ++++++- pkg/config/config.go | 91 ++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 pkg/config/config.go diff --git a/cmd/massa-wallet/massa-wallet.go b/cmd/massa-wallet/massa-wallet.go index e0452bf3c..a42c441cd 100644 --- a/cmd/massa-wallet/massa-wallet.go +++ b/cmd/massa-wallet/massa-wallet.go @@ -9,6 +9,7 @@ import ( "github.com/massalabs/station-massa-wallet/internal/handler" walletApp "github.com/massalabs/station-massa-wallet/pkg/app" "github.com/massalabs/station-massa-wallet/pkg/assets" + "github.com/massalabs/station-massa-wallet/pkg/config" "github.com/massalabs/station-massa-wallet/pkg/network" "github.com/massalabs/station-massa-wallet/pkg/prompt" "github.com/massalabs/station/pkg/logger" @@ -20,6 +21,8 @@ func StartServer(app *walletApp.WalletApp) { LRU(). Build() + config.Load() + massaClient := network.NewNodeFetcher() var promptApp prompt.WalletPrompterInterface = prompt.NewWalletPrompter(app) diff --git a/go.mod b/go.mod index d233adf00..dc0786340 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,9 @@ require ( github.com/bluele/gcache v0.0.2 github.com/go-openapi/runtime v0.25.0 github.com/jessevdk/go-flags v1.5.0 + github.com/knadh/koanf/parsers/json v0.1.0 + github.com/knadh/koanf/providers/file v1.1.2 + github.com/knadh/koanf/v2 v2.1.2 github.com/massalabs/station v0.6.6-0.20250122093330-de0ab4bbb76c github.com/massalabs/station-massa-hello-world v0.0.11-0.20240503070604-6b14a27fcdff github.com/pkg/errors v0.9.1 @@ -22,11 +25,14 @@ require ( github.com/bep/debounce v1.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/go-units v0.4.0 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/klauspost/cpuid/v2 v2.1.0 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect github.com/labstack/echo/v4 v4.10.2 // indirect github.com/labstack/gommon v0.4.0 // indirect github.com/leaanthony/go-ansi-parser v1.6.0 // indirect @@ -35,6 +41,8 @@ require ( github.com/leaanthony/u v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect @@ -74,7 +82,7 @@ require ( github.com/oklog/ulid v1.3.1 // indirect go.mongodb.org/mongo-driver v1.11.3 // indirect golang.org/x/net v0.25.0 - golang.org/x/sys v0.20.0 // indirect + golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.15.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.1.7 diff --git a/go.sum b/go.sum index a5697c39e..db41a175d 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/analysis v0.21.2 h1:hXFrOYFHUAMQdu6zwAiKKJHJQ8kqZs1ux/ru1P1wLJU= @@ -66,6 +68,8 @@ github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+ github.com/go-openapi/validate v0.22.0 h1:b0QecH6VslW/TxtpKgzpO1SNG7GU2FsaqKdP1E2T50Y= github.com/go-openapi/validate v0.22.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= @@ -119,6 +123,14 @@ github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHcvZ14NJ6fU= +github.com/knadh/koanf/parsers/json v0.1.0/go.mod h1:ll2/MlXcZ2BfXD6YJcjVFzhG9P0TdJ207aIBKQhV2hY= +github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w= +github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= +github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= +github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -163,10 +175,14 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= @@ -300,8 +316,8 @@ golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 000000000..6f4b49811 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,91 @@ +package config + +import ( + jsonStd "encoding/json" + "log" + "os" + "path/filepath" + "sync" + + "github.com/knadh/koanf/parsers/json" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" + "github.com/massalabs/station-massa-wallet/pkg/wallet" +) + +type SignRule struct { + Contract string `json:"contract"` + PasswordPrompt bool `json:"passwordPrompt"` + ConfirmationBypass bool `json:"confirmationBypass"` +} + +type Account struct { + SignRules []SignRule `json:"signRules"` +} + +type Config struct { + Accounts map[string]Account `json:"accounts"` +} + +var ( + cfg *Config + once sync.Once + k = koanf.New(".") +) + +const ( + fileName = "wallet_config.json" +) + +func Load() *Config { + once.Do(func() { + configDir, err := wallet.Path() + if err != nil { + log.Fatalf("Failed to get wallet config directory: %v", err) + } + + path := filepath.Join(configDir, fileName) + + if _, err := os.Stat(path); os.IsNotExist(err) { + log.Printf("config file not found. Creating default config at %s", path) + createDefaultConfigFile(path, defaultConfig()) + } + + if err := k.Load(file.Provider(path), json.Parser()); err != nil { + log.Fatalf("Error loading config: %v", err) + } + + cfg = &Config{} + if err := k.Unmarshal("", cfg); err != nil { + log.Fatalf("Error unmarshaling config: %v", err) + } + }) + return cfg +} + +func Get() *Config { + if cfg == nil { + log.Fatal("Config not loaded. Call Load() first.") + } + return cfg +} + +func defaultConfig() *Config { + return &Config{ + Accounts: map[string]Account{}, + } +} + +func createDefaultConfigFile(filePath string, defaultConfig *Config) { + file, err := os.Create(filePath) + if err != nil { + log.Fatalf("failed to create config file: %v", err) + } + defer file.Close() + + encoder := jsonStd.NewEncoder(file) + encoder.SetIndent("", " ") // Pretty-print JSON + if err := encoder.Encode(defaultConfig); err != nil { + log.Fatalf("failed to write default config to file: %v", err) + } +} From 2f59437cfaca2071c37b063f24a4b614dc69cb69 Mon Sep 17 00:00:00 2001 From: Peterjah Date: Tue, 17 Dec 2024 16:14:05 +0100 Subject: [PATCH 02/10] fix linter --- api/server/restapi/configure_massa_wallet.go | 2 ++ internal/handler/wallet/backup.go | 1 + internal/handler/wallet/export.go | 2 ++ internal/handler/wallet/get_all_assets.go | 3 +++ internal/handler/wallet/import_test.go | 2 ++ internal/handler/wallet/sign_test.go | 1 - pkg/assets/dollar.go | 1 + pkg/config/config.go | 3 +++ pkg/prompt/import.go | 5 +++-- pkg/prompt/prompt.go | 4 ++-- pkg/types/address_test.go | 2 ++ pkg/wallet/wallet.go | 2 ++ pkg/wallet/wallet_test.go | 1 - 13 files changed, 23 insertions(+), 6 deletions(-) diff --git a/api/server/restapi/configure_massa_wallet.go b/api/server/restapi/configure_massa_wallet.go index 67f9c9b27..8c5c455a2 100644 --- a/api/server/restapi/configure_massa_wallet.go +++ b/api/server/restapi/configure_massa_wallet.go @@ -97,8 +97,10 @@ func webAppMiddleware(handler http.Handler) http.Handler { responder := html.HandleWebApp(params) // Handle the successful response responder.WriteResponse(w, runtime.JSONProducer()) + return } + handler.ServeHTTP(w, r) }) } diff --git a/internal/handler/wallet/backup.go b/internal/handler/wallet/backup.go index eea31b31c..60b2aa56c 100644 --- a/internal/handler/wallet/backup.go +++ b/internal/handler/wallet/backup.go @@ -62,6 +62,7 @@ func (w *walletBackupAccount) Handle(params operations.BackupAccountParams) midd } else { password, _ := promptOutput.(*memguard.LockedBuffer) guardedPrivateKey, err := acc.PrivateKeyTextInClear(password) + //nolint:wsl if err != nil { return newErrorResponse(err.Error(), errorGetWallets, http.StatusInternalServerError) } diff --git a/internal/handler/wallet/export.go b/internal/handler/wallet/export.go index bf544c032..d2b8c0719 100644 --- a/internal/handler/wallet/export.go +++ b/internal/handler/wallet/export.go @@ -46,8 +46,10 @@ func (w *walletExportFile) Handle(params operations.ExportAccountFileParams) mid http.Error(w, err.Error(), http.StatusInternalServerError) return } + w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filepath.Base(file.Name()))) + if _, err := io.Copy(w, file); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/internal/handler/wallet/get_all_assets.go b/internal/handler/wallet/get_all_assets.go index 124b0f6bf..ab6851794 100644 --- a/internal/handler/wallet/get_all_assets.go +++ b/internal/handler/wallet/get_all_assets.go @@ -62,6 +62,7 @@ func (g *getAllAssets) Handle(params operations.GetAllAssetsParams) middleware.R if assetsWithBalance[i].AssetInfo.Symbol == "MAS" { return true } + if assetsWithBalance[j].AssetInfo.Symbol == "MAS" { return false } @@ -72,9 +73,11 @@ func (g *getAllAssets) Handle(params operations.GetAllAssetsParams) middleware.R if (valueI == nil || *valueI == 0) && (valueJ == nil || *valueJ == 0) { return assetsWithBalance[i].AssetInfo.Symbol < assetsWithBalance[j].AssetInfo.Symbol } + if valueI == nil || *valueI == 0 { return false } + if valueJ == nil || *valueJ == 0 { return true } diff --git a/internal/handler/wallet/import_test.go b/internal/handler/wallet/import_test.go index 545c44b6d..162c0db8b 100644 --- a/internal/handler/wallet/import_test.go +++ b/internal/handler/wallet/import_test.go @@ -87,6 +87,7 @@ PublicKey: [0, 164, 243, 44, 155, 204, 6, 20, 131, 218, 97, 32, 58, 224, 189, 41 data := []byte(walletFile) err = os.WriteFile(filePath, data, 0o644) assert.NoError(t, err) + defer os.Remove(filePath) testResult := make(chan walletapp.EventData) @@ -131,6 +132,7 @@ PublicKey: [0, 164, 243, 44, 155, 204, 6, 20, 131, 218, 97, 32, 58, 224, 189, 41 data := []byte(walletFile) err = os.WriteFile(filePath, data, 0o644) assert.NoError(t, err) + defer os.Remove(filePath) testResult := make(chan walletapp.EventData) diff --git a/internal/handler/wallet/sign_test.go b/internal/handler/wallet/sign_test.go index 1ccf12ccb..3e2da4dbe 100644 --- a/internal/handler/wallet/sign_test.go +++ b/internal/handler/wallet/sign_test.go @@ -234,7 +234,6 @@ func Test_walletSign_Handle(t *testing.T) { resp = signTransaction(t, api, nickname, transactionDataBatch) verifyStatusCode(t, resp, http.StatusOK) // remove this line when enabling batch signing - // Uncomment the code below when enabling batch signing: // var bodyError operations.SignInternalServerError // err = json.Unmarshal(resp.Body.Bytes(), &bodyError) diff --git a/pkg/assets/dollar.go b/pkg/assets/dollar.go index 9c817fa74..c468b3854 100644 --- a/pkg/assets/dollar.go +++ b/pkg/assets/dollar.go @@ -24,6 +24,7 @@ func DollarValue(balance, MEXCSymbol, symbol string, decimals int64) (*float64, } else { err := error(nil) price, err = DollarPrice(MEXCSymbol) + //nolint:wsl if err != nil { return nil, fmt.Errorf("Error getting dollar price: %s\n", err) } diff --git a/pkg/config/config.go b/pkg/config/config.go index 6f4b49811..9136b7751 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -60,6 +60,7 @@ func Load() *Config { log.Fatalf("Error unmarshaling config: %v", err) } }) + return cfg } @@ -67,6 +68,7 @@ func Get() *Config { if cfg == nil { log.Fatal("Config not loaded. Call Load() first.") } + return cfg } @@ -85,6 +87,7 @@ func createDefaultConfigFile(filePath string, defaultConfig *Config) { encoder := jsonStd.NewEncoder(file) encoder.SetIndent("", " ") // Pretty-print JSON + if err := encoder.Encode(defaultConfig); err != nil { log.Fatalf("failed to write default config to file: %v", err) } diff --git a/pkg/prompt/import.go b/pkg/prompt/import.go index a46f37be9..20e063bd9 100644 --- a/pkg/prompt/import.go +++ b/pkg/prompt/import.go @@ -1,6 +1,7 @@ package prompt import ( + "errors" "fmt" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" @@ -39,7 +40,7 @@ func handleImportFile(prompterApp WalletPrompterInterface, filePath string) (*ac prompterApp.EmitEvent(walletapp.PromptResultEvent, walletapp.EventData{Success: false, CodeMessage: utils.WailsErrorCode(err)}) - return nil, false, fmt.Errorf(msg) + return nil, false, errors.New(msg) } return acc, false, nil @@ -62,7 +63,7 @@ func handleImportPrivateKey(prompterApp WalletPrompterInterface, walletInfo wall prompterApp.EmitEvent(walletapp.PromptResultEvent, walletapp.EventData{Success: false, CodeMessage: utils.WailsErrorCode(err)}) - return nil, false, fmt.Errorf(msg) + return nil, false, errors.New(msg) } return acc, false, nil diff --git a/pkg/prompt/prompt.go b/pkg/prompt/prompt.go index 607d7cb45..4ad1e3cfc 100644 --- a/pkg/prompt/prompt.go +++ b/pkg/prompt/prompt.go @@ -2,7 +2,7 @@ package prompt import ( "context" - "fmt" + "errors" "math/rand" "strconv" @@ -62,7 +62,7 @@ func WakeUpPrompt( ) (interface{}, error) { if prompterApp.IsListening() { logger.Warn(AlreadyListeningErr) - return nil, fmt.Errorf(AlreadyListeningErr) + return nil, errors.New(AlreadyListeningErr) } prompterApp.Lock() diff --git a/pkg/types/address_test.go b/pkg/types/address_test.go index fe2159421..b81356a49 100644 --- a/pkg/types/address_test.go +++ b/pkg/types/address_test.go @@ -45,6 +45,7 @@ func TestAddress_UnmarshalText(t *testing.T) { if err := a.UnmarshalText(tt.text); err != tt.expectedErr { t.Errorf("Address.UnmarshalText() error = %v, wantErr %v", err, tt.expectedErr) } + assert.Equal(t, tt.expectedType, a.Kind, "Should be %s, got %s", tt.expectedType, a.Kind) assert.Equal(t, tt.expectedData, a.Data) }) @@ -229,6 +230,7 @@ func TestAddress_UnmarshalBinary(t *testing.T) { if err := a.UnmarshalBinary(tt.data); err != tt.expectedErr { t.Errorf("Address.UnmarshalBinary() error = %v, wantErr %v", err, tt.expectedErr) } + assert.Equal(t, tt.expectedType, a.Kind, "Should be %s, got %s", tt.expectedType, a.Kind) assert.Equal(t, tt.expectedData, a.Data) }) diff --git a/pkg/wallet/wallet.go b/pkg/wallet/wallet.go index 639f6650b..70b61e86f 100644 --- a/pkg/wallet/wallet.go +++ b/pkg/wallet/wallet.go @@ -200,6 +200,7 @@ func (w *Wallet) GetAccountFromAddress(needle string) (*account.Account, error) return false } } + return true }) @@ -243,6 +244,7 @@ func (w *Wallet) AllAccounts() []*account.Account { if ok { accounts = append(accounts, acc) } + return true }) diff --git a/pkg/wallet/wallet_test.go b/pkg/wallet/wallet_test.go index 943ffc644..a1956e490 100644 --- a/pkg/wallet/wallet_test.go +++ b/pkg/wallet/wallet_test.go @@ -141,7 +141,6 @@ func TestWallet(t *testing.T) { t.Run("Get Account: new file added manually", func(t *testing.T) { // User can add an account file in the account folder by its own, // we want to wallet to be able to manage this account. - accountPath, err := w.AccountPath(sampleNicknameUnitTest) assert.NoError(t, err) From d3f0bdeeda5a08f93ad7d2982e1df72012ea1868 Mon Sep 17 00:00:00 2001 From: Peterjah Date: Wed, 18 Dec 2024 13:11:26 +0100 Subject: [PATCH 03/10] remove batch & correlationId --- api/server/models/correlation_id.go | 56 ----------- api/server/models/sign_request.go | 57 +---------- api/server/models/sign_response.go | 52 ---------- api/server/restapi/embedded_spec.go | 42 ++------- api/server/restapi/operations/sign.go | 2 +- .../restapi/operations/sign_responses.go | 4 +- api/walletApi-V0.yml | 17 +--- documentation/wallet.adoc | 13 +-- internal/handler/wallet/api_test.go | 4 + internal/handler/wallet/backup_test.go | 18 ++-- internal/handler/wallet/create_test.go | 2 +- internal/handler/wallet/delete_test.go | 33 +------ internal/handler/wallet/errors.go | 2 - internal/handler/wallet/get_test.go | 2 +- internal/handler/wallet/import_test.go | 8 +- internal/handler/wallet/rolls_test.go | 4 +- internal/handler/wallet/sign.go | 94 +++++++------------ internal/handler/wallet/sign_test.go | 83 +--------------- internal/handler/wallet/testsUtils.go | 3 +- internal/handler/wallet/transfer_test.go | 2 +- pkg/app/app.go | 16 ++-- pkg/app/events.go | 18 +--- pkg/config/config.go | 24 +++-- pkg/network/operation.go | 3 +- pkg/prompt/env.go | 4 +- pkg/prompt/prompt.go | 22 +---- pkg/utils/errors.go | 12 +-- wails-frontend/src/events/events.tsx | 1 - .../pages/PasswordPromptHandler/Delete.tsx | 2 +- .../src/pages/PasswordPromptHandler/Sign.tsx | 2 +- wails-frontend/src/pages/backupKeyPairs.tsx | 2 +- wails-frontend/src/pages/backupMethods.tsx | 4 +- wails-frontend/src/pages/confirmDelete.tsx | 2 +- wails-frontend/src/pages/importFile.tsx | 2 +- wails-frontend/src/pages/newPassword.tsx | 9 +- .../wailsjs/go/walletapp/WalletApp.d.ts | 6 +- .../wailsjs/go/walletapp/WalletApp.js | 12 +-- 37 files changed, 144 insertions(+), 495 deletions(-) delete mode 100644 api/server/models/correlation_id.go diff --git a/api/server/models/correlation_id.go b/api/server/models/correlation_id.go deleted file mode 100644 index 6907fd8c0..000000000 --- a/api/server/models/correlation_id.go +++ /dev/null @@ -1,56 +0,0 @@ -// Code generated by go-swagger; DO NOT EDIT. - -package models - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -import ( - "context" - - "github.com/go-openapi/strfmt" - "github.com/go-openapi/swag" -) - -// CorrelationID Correlation id of the operation batch -// -// swagger:model CorrelationId -type CorrelationID strfmt.Base64 - -// UnmarshalJSON sets a CorrelationID value from JSON input -func (m *CorrelationID) UnmarshalJSON(b []byte) error { - return ((*strfmt.Base64)(m)).UnmarshalJSON(b) -} - -// MarshalJSON retrieves a CorrelationID value as JSON output -func (m CorrelationID) MarshalJSON() ([]byte, error) { - return (strfmt.Base64(m)).MarshalJSON() -} - -// Validate validates this correlation Id -func (m CorrelationID) Validate(formats strfmt.Registry) error { - return nil -} - -// ContextValidate validates this correlation Id based on context it is used -func (m CorrelationID) ContextValidate(ctx context.Context, formats strfmt.Registry) error { - return nil -} - -// MarshalBinary interface implementation -func (m *CorrelationID) MarshalBinary() ([]byte, error) { - if m == nil { - return nil, nil - } - return swag.WriteJSON(m) -} - -// UnmarshalBinary interface implementation -func (m *CorrelationID) UnmarshalBinary(b []byte) error { - var res CorrelationID - if err := swag.ReadJSON(b, &res); err != nil { - return err - } - *m = res - return nil -} diff --git a/api/server/models/sign_request.go b/api/server/models/sign_request.go index 091038d00..25f6a2fa3 100644 --- a/api/server/models/sign_request.go +++ b/api/server/models/sign_request.go @@ -19,17 +19,10 @@ import ( // swagger:model SignRequest type SignRequest struct { - // A boolean property that indicates whether the sign operation is part of a batch of operations. Set to true if this operation is part of a batch, otherwise set to false. - Batch bool `json:"batch,omitempty"` - // The chain id of the network to which the operation will be sent. // Required: true ChainID *int64 `json:"chainId"` - // correlation Id - // Format: byte - CorrelationID CorrelationID `json:"correlationId,omitempty"` - // Description text of what is being signed (optional) // Max Length: 280 Description string `json:"description,omitempty"` @@ -48,10 +41,6 @@ func (m *SignRequest) Validate(formats strfmt.Registry) error { res = append(res, err) } - if err := m.validateCorrelationID(formats); err != nil { - res = append(res, err) - } - if err := m.validateDescription(formats); err != nil { res = append(res, err) } @@ -75,23 +64,6 @@ func (m *SignRequest) validateChainID(formats strfmt.Registry) error { return nil } -func (m *SignRequest) validateCorrelationID(formats strfmt.Registry) error { - if swag.IsZero(m.CorrelationID) { // not required - return nil - } - - if err := m.CorrelationID.Validate(formats); err != nil { - if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("correlationId") - } else if ce, ok := err.(*errors.CompositeError); ok { - return ce.ValidateName("correlationId") - } - return err - } - - return nil -} - func (m *SignRequest) validateDescription(formats strfmt.Registry) error { if swag.IsZero(m.Description) { // not required return nil @@ -113,35 +85,8 @@ func (m *SignRequest) validateOperation(formats strfmt.Registry) error { return nil } -// ContextValidate validate this sign request based on the context it is used +// ContextValidate validates this sign request based on context it is used func (m *SignRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error { - var res []error - - if err := m.contextValidateCorrelationID(ctx, formats); err != nil { - res = append(res, err) - } - - if len(res) > 0 { - return errors.CompositeValidationError(res...) - } - return nil -} - -func (m *SignRequest) contextValidateCorrelationID(ctx context.Context, formats strfmt.Registry) error { - - if swag.IsZero(m.CorrelationID) { // not required - return nil - } - - if err := m.CorrelationID.ContextValidate(ctx, formats); err != nil { - if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("correlationId") - } else if ce, ok := err.(*errors.CompositeError); ok { - return ce.ValidateName("correlationId") - } - return err - } - return nil } diff --git a/api/server/models/sign_response.go b/api/server/models/sign_response.go index 4d91f1f69..07f7edbf3 100644 --- a/api/server/models/sign_response.go +++ b/api/server/models/sign_response.go @@ -19,10 +19,6 @@ import ( // swagger:model SignResponse type SignResponse struct { - // correlation Id - // Format: byte - CorrelationID CorrelationID `json:"correlationId,omitempty"` - // The modified operation (usr can change the fees). // Read Only: true // Format: byte @@ -40,32 +36,6 @@ type SignResponse struct { // Validate validates this sign response func (m *SignResponse) Validate(formats strfmt.Registry) error { - var res []error - - if err := m.validateCorrelationID(formats); err != nil { - res = append(res, err) - } - - if len(res) > 0 { - return errors.CompositeValidationError(res...) - } - return nil -} - -func (m *SignResponse) validateCorrelationID(formats strfmt.Registry) error { - if swag.IsZero(m.CorrelationID) { // not required - return nil - } - - if err := m.CorrelationID.Validate(formats); err != nil { - if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("correlationId") - } else if ce, ok := err.(*errors.CompositeError); ok { - return ce.ValidateName("correlationId") - } - return err - } - return nil } @@ -73,10 +43,6 @@ func (m *SignResponse) validateCorrelationID(formats strfmt.Registry) error { func (m *SignResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error { var res []error - if err := m.contextValidateCorrelationID(ctx, formats); err != nil { - res = append(res, err) - } - if err := m.contextValidateOperation(ctx, formats); err != nil { res = append(res, err) } @@ -95,24 +61,6 @@ func (m *SignResponse) ContextValidate(ctx context.Context, formats strfmt.Regis return nil } -func (m *SignResponse) contextValidateCorrelationID(ctx context.Context, formats strfmt.Registry) error { - - if swag.IsZero(m.CorrelationID) { // not required - return nil - } - - if err := m.CorrelationID.ContextValidate(ctx, formats); err != nil { - if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("correlationId") - } else if ce, ok := err.(*errors.CompositeError); ok { - return ce.ValidateName("correlationId") - } - return err - } - - return nil -} - func (m *SignResponse) contextValidateOperation(ctx context.Context, formats strfmt.Registry) error { if err := validate.ReadOnly(ctx, "operation", "body", strfmt.Base64(m.Operation)); err != nil { diff --git a/api/server/restapi/embedded_spec.go b/api/server/restapi/embedded_spec.go index 8d5345e0d..c642b353f 100644 --- a/api/server/restapi/embedded_spec.go +++ b/api/server/restapi/embedded_spec.go @@ -590,7 +590,7 @@ func init() { }, "/api/accounts/{nickname}/sign": { "post": { - "description": "Sign an operation or a message using the account associated with the provided nickname in the path. If no correlationId is provided, the user will be prompted to enter their account password.", + "description": "Sign an operation or a message using the account associated with the provided nickname in the path.", "produces": [ "application/json" ], @@ -619,7 +619,7 @@ func init() { ], "responses": { "200": { - "description": "Returns the signature, public key, and correlationId (if provided).", + "description": "Returns the signature, public key.", "schema": { "$ref": "#/definitions/SignResponse" } @@ -631,7 +631,7 @@ func init() { } }, "401": { - "description": "Unauthorized - The request requires user authentication. Only if no correlationId is provided.", + "description": "Unauthorized - The request requires user authentication.", "schema": { "$ref": "#/definitions/Error" } @@ -904,11 +904,6 @@ func init() { } ] }, - "CorrelationId": { - "description": "Correlation id of the operation batch", - "type": "string", - "format": "byte" - }, "Error": { "description": "Error object.", "type": "object", @@ -1029,17 +1024,10 @@ func init() { "chainId" ], "properties": { - "batch": { - "description": "A boolean property that indicates whether the sign operation is part of a batch of operations. Set to true if this operation is part of a batch, otherwise set to false.", - "type": "boolean" - }, "chainId": { "description": "The chain id of the network to which the operation will be sent.", "type": "integer" }, - "correlationId": { - "$ref": "#/definitions/CorrelationId" - }, "description": { "description": "Description text of what is being signed (optional)", "type": "string", @@ -1056,9 +1044,6 @@ func init() { "description": "Signature of a sent operation.", "type": "object", "properties": { - "correlationId": { - "$ref": "#/definitions/CorrelationId" - }, "operation": { "description": "The modified operation (usr can change the fees).", "type": "string", @@ -1757,7 +1742,7 @@ func init() { }, "/api/accounts/{nickname}/sign": { "post": { - "description": "Sign an operation or a message using the account associated with the provided nickname in the path. If no correlationId is provided, the user will be prompted to enter their account password.", + "description": "Sign an operation or a message using the account associated with the provided nickname in the path.", "produces": [ "application/json" ], @@ -1791,7 +1776,7 @@ func init() { ], "responses": { "200": { - "description": "Returns the signature, public key, and correlationId (if provided).", + "description": "Returns the signature, public key.", "schema": { "$ref": "#/definitions/SignResponse" } @@ -1803,7 +1788,7 @@ func init() { } }, "401": { - "description": "Unauthorized - The request requires user authentication. Only if no correlationId is provided.", + "description": "Unauthorized - The request requires user authentication.", "schema": { "$ref": "#/definitions/Error" } @@ -2088,11 +2073,6 @@ func init() { } ] }, - "CorrelationId": { - "description": "Correlation id of the operation batch", - "type": "string", - "format": "byte" - }, "Error": { "description": "Error object.", "type": "object", @@ -2213,17 +2193,10 @@ func init() { "chainId" ], "properties": { - "batch": { - "description": "A boolean property that indicates whether the sign operation is part of a batch of operations. Set to true if this operation is part of a batch, otherwise set to false.", - "type": "boolean" - }, "chainId": { "description": "The chain id of the network to which the operation will be sent.", "type": "integer" }, - "correlationId": { - "$ref": "#/definitions/CorrelationId" - }, "description": { "description": "Description text of what is being signed (optional)", "type": "string", @@ -2240,9 +2213,6 @@ func init() { "description": "Signature of a sent operation.", "type": "object", "properties": { - "correlationId": { - "$ref": "#/definitions/CorrelationId" - }, "operation": { "description": "The modified operation (usr can change the fees).", "type": "string", diff --git a/api/server/restapi/operations/sign.go b/api/server/restapi/operations/sign.go index b326b445c..2ea5a3742 100644 --- a/api/server/restapi/operations/sign.go +++ b/api/server/restapi/operations/sign.go @@ -32,7 +32,7 @@ func NewSign(ctx *middleware.Context, handler SignHandler) *Sign { /* Sign swagger:route POST /api/accounts/{nickname}/sign sign -Sign an operation or a message using the account associated with the provided nickname in the path. If no correlationId is provided, the user will be prompted to enter their account password. +Sign an operation or a message using the account associated with the provided nickname in the path. */ type Sign struct { Context *middleware.Context diff --git a/api/server/restapi/operations/sign_responses.go b/api/server/restapi/operations/sign_responses.go index 5fe444d6b..eea9bae73 100644 --- a/api/server/restapi/operations/sign_responses.go +++ b/api/server/restapi/operations/sign_responses.go @@ -16,7 +16,7 @@ import ( const SignOKCode int = 200 /* -SignOK Returns the signature, public key, and correlationId (if provided). +SignOK Returns the signature, public key. swagger:response signOK */ @@ -106,7 +106,7 @@ func (o *SignBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime. const SignUnauthorizedCode int = 401 /* -SignUnauthorized Unauthorized - The request requires user authentication. Only if no correlationId is provided. +SignUnauthorized Unauthorized - The request requires user authentication. swagger:response signUnauthorized */ diff --git a/api/walletApi-V0.yml b/api/walletApi-V0.yml index 78373fb61..8906f647c 100644 --- a/api/walletApi-V0.yml +++ b/api/walletApi-V0.yml @@ -235,7 +235,7 @@ paths: /api/accounts/{nickname}/sign: post: operationId: Sign - description: Sign an operation or a message using the account associated with the provided nickname in the path. If no correlationId is provided, the user will be prompted to enter their account password. + description: Sign an operation or a message using the account associated with the provided nickname in the path. parameters: - $ref: "#/parameters/nickname" - in: body @@ -255,7 +255,7 @@ paths: - application/json responses: 200: - description: Returns the signature, public key, and correlationId (if provided). + description: Returns the signature, public key. schema: $ref: "#/definitions/SignResponse" 400: @@ -263,7 +263,7 @@ paths: schema: $ref: "#/definitions/Error" 401: - description: Unauthorized - The request requires user authentication. Only if no correlationId is provided. + description: Unauthorized - The request requires user authentication. schema: $ref: "#/definitions/Error" 404: @@ -565,11 +565,6 @@ definitions: description: Serialized attributes of the operation to be signed with the key pair corresponding to the given nickname. type: string format: byte - batch: - description: A boolean property that indicates whether the sign operation is part of a batch of operations. Set to true if this operation is part of a batch, otherwise set to false. - type: boolean - correlationId: - $ref: "#/definitions/CorrelationId" chainId: type: integer description: The chain id of the network to which the operation will be sent. @@ -659,12 +654,6 @@ definitions: format: byte x-nullable: false readOnly: true - correlationId: - $ref: "#/definitions/CorrelationId" - CorrelationId: - description: Correlation id of the operation batch - type: string - format: byte Nickname: description: Account's short name. type: string diff --git a/documentation/wallet.adoc b/documentation/wallet.adoc index 28913aae3..6b099a106 100644 --- a/documentation/wallet.adoc +++ b/documentation/wallet.adoc @@ -666,11 +666,7 @@ NOTE: For hands-on testing and examples, refer to the provided Swagger documenta The 'sign' endpoint serves the purpose of signing all five operation types, including Transaction, Roll buy/sell, CallSC, and -ExecuteSC. It provides the capability to batch operation requests by -utilizing the correlationID parameter. No user interaction is required once first operation processing is underway. -Batching operation requests in Massa Wallet does not entail processing the operations as a group. -Instead, it enhances the user experience by allowing users to, sequentially, sign multiple operations -after being prompted to provide a password once. +ExecuteSC. ===== Parameters @@ -680,23 +676,17 @@ after being prompted to provide a password once. | `nickname` | Identifier for the account. | Required | String | `description` | Text describing the signing intent. | Optional | String (Default: "", Max: 280 characters) | `operation` | Serialized attributes for signing. | Required | Byte array -| `batch` | Specifies if the operation is part of a batch. | Optional | Boolean (Default: `false`) -| `correlationId` | Identifier for correlating multiple requests. | Optional | String (Default: "") |=== -IMPORTANT: To ensure security, the `correlationId` should be sent in the request body, not in the query string. - ===== Response -The correlationId will be included if the batch parameter is set to true. [%header,cols="1,3,1"] |=== | Name | Description | Format | `signature` | The signature of the signed message | String | `publicKey` | The public key of the account | String -| `correlationId` | Correlation id of the operation batch | String |=== @@ -726,7 +716,6 @@ calls with the same data will initiate separate executions. | Name | Description | Format | `signature` | The signature of the signed message | String | `publicKey` | The public key of the account | String -| `correlationId` | Correlation id of the operation batch | String |=== ==== Create Account diff --git a/internal/handler/wallet/api_test.go b/internal/handler/wallet/api_test.go index befa01613..cb6d788ac 100644 --- a/internal/handler/wallet/api_test.go +++ b/internal/handler/wallet/api_test.go @@ -15,6 +15,7 @@ import ( "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" "github.com/massalabs/station-massa-wallet/pkg/assets" + "github.com/massalabs/station-massa-wallet/pkg/config" "github.com/massalabs/station-massa-wallet/pkg/network" "github.com/massalabs/station-massa-wallet/pkg/prompt" "github.com/massalabs/station-massa-wallet/pkg/wallet" @@ -72,6 +73,9 @@ func MockAPI() (*operations.MassaWalletAPI, prompt.WalletPrompterInterface, *ass massaNodeMock := NewNodeFetcherMock() + // Load config file + config.Load() + // Set wallet API endpoints AppendEndpoints(massaWalletAPI, prompterApp, massaNodeMock, AssetsStore, gcache.New(20).LRU().Build()) diff --git a/internal/handler/wallet/backup_test.go b/internal/handler/wallet/backup_test.go index 10d173acc..d407585ff 100644 --- a/internal/handler/wallet/backup_test.go +++ b/internal/handler/wallet/backup_test.go @@ -51,7 +51,7 @@ func Test_walletBackupAccount_Handle(t *testing.T) { go func() { prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: string(prompt.YamlFileBackup), } testResult <- (<-resChan) @@ -69,7 +69,7 @@ func Test_walletBackupAccount_Handle(t *testing.T) { go func() { // send backup method prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: string(prompt.PrivateKeyBackup), } prompterApp.App().CtrlChan <- walletapp.Cancel @@ -85,12 +85,12 @@ func Test_walletBackupAccount_Handle(t *testing.T) { go func() { // send backup method prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: string(prompt.PrivateKeyBackup), } // send password prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: "wrong password", } @@ -100,7 +100,7 @@ func Test_walletBackupAccount_Handle(t *testing.T) { // send password prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: password, } testResult <- (<-resChan) @@ -127,12 +127,12 @@ func Test_walletBackupAccount_Handle(t *testing.T) { go func() { // send backup method prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: string(prompt.PrivateKeyBackup), } // send password prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: "wrong password", } @@ -153,12 +153,12 @@ func Test_walletBackupAccount_Handle(t *testing.T) { go func() { // send backup method prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: string(prompt.PrivateKeyBackup), } // send password prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: password, } diff --git a/internal/handler/wallet/create_test.go b/internal/handler/wallet/create_test.go index cb1043cd9..5399a1402 100644 --- a/internal/handler/wallet/create_test.go +++ b/internal/handler/wallet/create_test.go @@ -44,7 +44,7 @@ func Test_walletCreate_Handle(t *testing.T) { prompterApp.App().CtrlChan <- walletapp.Cancel } else { prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: test.password, } // forward test result to test goroutine diff --git a/internal/handler/wallet/delete_test.go b/internal/handler/wallet/delete_test.go index 527d7577a..dc90e2ad0 100644 --- a/internal/handler/wallet/delete_test.go +++ b/internal/handler/wallet/delete_test.go @@ -41,7 +41,7 @@ func Test_walletDelete_Handle(t *testing.T) { // Send password to prompter app and wait for result go func() { prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: "invalid password", } // forward test result to test goroutine @@ -61,7 +61,7 @@ func Test_walletDelete_Handle(t *testing.T) { go func() { // Send wrong password to prompter app and wait for result prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: "this is not the password", } // forward test result to test goroutine @@ -81,37 +81,11 @@ func Test_walletDelete_Handle(t *testing.T) { assert.NoError(t, err) }) - t.Run("invalid prompt correlation id", func(t *testing.T) { - // first send a invalid prompt correlation id and then the correct one - go func() { - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: "666"}, - Message: password, - } - - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, - Message: password, - } - // forward test result to test goroutine - testResult <- (<-resChan) - }() - - resp := deleteWallet(t, api, nickname) - verifyStatusCode(t, resp, http.StatusNoContent) - result := <-testResult - checkResultChannel(t, result, true, "") - _, err = prompterApp.App().Wallet.GetAccount(nickname) - assert.Error(t, err, "Wallet should have been deleted") - }) - t.Run("delete success", func(t *testing.T) { - createAccount(password, nickname, t, prompterApp) - // Send password to prompter app and wait for result go func() { prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: password, } // forward test result to test goroutine @@ -127,6 +101,7 @@ func Test_walletDelete_Handle(t *testing.T) { checkResultChannel(t, result, true, "") _, err = prompterApp.App().Wallet.GetAccount(nickname) + assert.Error(t, err, "Wallet should have been deleted") }) } diff --git a/internal/handler/wallet/errors.go b/internal/handler/wallet/errors.go index 4ccbd2bb0..664eeebd0 100644 --- a/internal/handler/wallet/errors.go +++ b/internal/handler/wallet/errors.go @@ -16,8 +16,6 @@ const ( errorSignDecodeMessage errorSign errorSignLoadCache - errorSignCorrelationIDNotFound - errorSignGenerateCorrelationID errorImportWalletCanceled errorImportNickNameAlreadyTaken errorImportWallet diff --git a/internal/handler/wallet/get_test.go b/internal/handler/wallet/get_test.go index ed9959747..32ec9f49f 100644 --- a/internal/handler/wallet/get_test.go +++ b/internal/handler/wallet/get_test.go @@ -97,7 +97,7 @@ func Test_getWallet_handler(t *testing.T) { // Send password to prompter app and wait for result go func(res chan walletapp.EventData) { prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: password, } // forward test result to test goroutine diff --git a/internal/handler/wallet/import_test.go b/internal/handler/wallet/import_test.go index 162c0db8b..7f7ee3969 100644 --- a/internal/handler/wallet/import_test.go +++ b/internal/handler/wallet/import_test.go @@ -59,7 +59,7 @@ PublicKey: [0, 164, 243, 44, 155, 204, 6, 20, 131, 218, 97, 32, 58, 224, 189, 41 // Send filepath to prompter app and wait for result go func(res chan walletapp.EventData) { prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: filePath, } // forward test result to test goroutine @@ -96,7 +96,7 @@ PublicKey: [0, 164, 243, 44, 155, 204, 6, 20, 131, 218, 97, 32, 58, 224, 189, 41 go func(res chan walletapp.EventData) { // Send invalid file to prompter app and wait for result prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: filePath, } failRes := <-resChan @@ -141,7 +141,7 @@ PublicKey: [0, 164, 243, 44, 155, 204, 6, 20, 131, 218, 97, 32, 58, 224, 189, 41 go func(res chan walletapp.EventData) { // Send invalid filename to prompter app and wait for result prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Message: filePath, } failRes := <-resChan @@ -202,7 +202,7 @@ PublicKey: [0, 164, 243, 44, 155, 204, 6, 20, 131, 218, 97, 32, 58, 224, 189, 41 go func(res chan walletapp.EventData) { prompterApp.App().PromptInput <- &walletapp.ImportPKeyPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, PrivateKey: tt.privateKey, Nickname: tt.nickname, Password: tt.password, diff --git a/internal/handler/wallet/rolls_test.go b/internal/handler/wallet/rolls_test.go index aba4e22cf..2d598ba81 100644 --- a/internal/handler/wallet/rolls_test.go +++ b/internal/handler/wallet/rolls_test.go @@ -85,7 +85,7 @@ func Test_traderolls_handler(t *testing.T) { // Send password to prompter app and wait for result go func(res chan walletapp.EventData) { prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Password: password, Fees: "1000", } @@ -112,7 +112,7 @@ func Test_traderolls_handler(t *testing.T) { // Send password to prompter app and wait for result go func(res chan walletapp.EventData) { prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Password: password, Fees: "1000", } diff --git a/internal/handler/wallet/sign.go b/internal/handler/wallet/sign.go index fcb1723af..f7f94e61c 100644 --- a/internal/handler/wallet/sign.go +++ b/internal/handler/wallet/sign.go @@ -2,10 +2,8 @@ package wallet import ( "crypto/ed25519" - cryptorand "crypto/rand" "encoding/binary" "fmt" - "io" "net/http" "os" "strconv" @@ -34,8 +32,9 @@ import ( ) const ( - defaultExpirationTime = time.Second * 60 * 10 + defaultExpirationTime = time.Hour * 24 * 30 RollPrice = 100 + cacheKeyPrefix = "pkey" ) type PromptRequestSignData struct { @@ -83,18 +82,17 @@ func (w *walletSign) Handle(params operations.SignParams) middleware.Responder { return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorSignDecodeMessage, http.StatusBadRequest) } - var correlationID *memguard.LockedBuffer var privateKey *memguard.LockedBuffer - const disableBatchSigning = true + // accountCfg, err := config.GetAccountConfig(params.Nickname) - if params.Body.CorrelationID != nil && !disableBatchSigning { - correlationID = memguard.NewBufferFromBytes(params.Body.CorrelationID) - - pk, err := w.privateKeyFromCache(acc, correlationID) + if err != nil { + // todo + // todo: handle cache expired error + pk, err := w.privateKeyFromCache(acc) if err != nil { - if errors.Is(err, utils.ErrCorrelationIDNotFound) { - return newErrorResponse(err.Error(), errorSignCorrelationIDNotFound, http.StatusNotFound) + if errors.Is(err, errors.New("errtodo")) { + return newErrorResponse(err.Error(), "errtodo", http.StatusNotFound) } return newErrorResponse(err.Error(), errorSign, http.StatusInternalServerError) @@ -121,14 +119,11 @@ func (w *walletSign) Handle(params operations.SignParams) middleware.Responder { privateKey = pk - if params.Body.Batch { - cID, err := w.CacheAccount(acc, privateKey) + if false { // todo + err := w.CacheAccount(acc, privateKey) if err != nil { - return newErrorResponse(err.Error(), errorSignCorrelationIDNotFound, http.StatusInternalServerError) + return newErrorResponse(err.Error(), "errtodo", http.StatusInternalServerError) } - correlationID = cID - } else { - correlationID = memguard.NewBufferFromBytes([]byte{}) } } @@ -143,7 +138,7 @@ func (w *walletSign) Handle(params operations.SignParams) middleware.Responder { return newErrorResponse("Error: signature verification failed", "errorSignVerifySignature", http.StatusInternalServerError) } - return w.Success(acc, signature, correlationID, operation) + return w.Success(acc, signature, operation) } func (w *walletSign) PromptPassword(acc *account.Account, promptRequest *prompt.PromptRequest) (*walletapp.SignPromptOutput, error) { @@ -163,17 +158,14 @@ func (w *walletSign) PromptPassword(acc *account.Account, promptRequest *prompt. return output, nil } -func (w *walletSign) CacheAccount(acc *account.Account, privateKey *memguard.LockedBuffer) (*memguard.LockedBuffer, error) { - correlationID, err := generateCorrelationID() - if err != nil { - return nil, fmt.Errorf("Error cannot generate correlation id: %w", err) - } +func (w *walletSign) CacheAccount(acc *account.Account, privateKey *memguard.LockedBuffer) error { + cacheKey := cacheKeyPrefix + acc.Nickname - key := CacheKey(correlationID.Bytes()) + key := KeyHash([]byte(cacheKey)) - cipheredPrivateKey, err := Xor(privateKey, correlationID) + cipheredPrivateKey, err := Xor(privateKey, key) if err != nil { - return nil, fmt.Errorf("Error cannot XOR correlation id: %w", err) + return fmt.Errorf("Error cannot XOR private key: %w", err) } cacheValue := make([]byte, ed25519.PrivateKeySize) @@ -182,13 +174,13 @@ func (w *walletSign) CacheAccount(acc *account.Account, privateKey *memguard.Loc err = w.gc.SetWithExpire(key, cacheValue, expirationDuration()) if err != nil { - return nil, fmt.Errorf("Error set correlation id in cache: %w", err) + return fmt.Errorf("Error set private key in cache: %w", err) } - return correlationID, nil + return nil } -func (w *walletSign) Success(acc *account.Account, signature []byte, correlationId *memguard.LockedBuffer, operation []byte) middleware.Responder { +func (w *walletSign) Success(acc *account.Account, signature []byte, operation []byte) middleware.Responder { publicKeyBytes, err := acc.PublicKey.MarshalText() if err != nil { return newErrorResponse(err.Error(), errorGetAccount, http.StatusInternalServerError) @@ -196,10 +188,9 @@ func (w *walletSign) Success(acc *account.Account, signature []byte, correlation return operations.NewSignOK().WithPayload( &models.SignResponse{ - PublicKey: string(publicKeyBytes), - Signature: signature, - CorrelationID: correlationId.Bytes(), - Operation: operation, + PublicKey: string(publicKeyBytes), + Signature: signature, + Operation: operation, }) } @@ -384,14 +375,14 @@ func (w *walletSign) getTransactionPromptData( // privateKeyFromCache return the private key from the cache or an error. func (w *walletSign) privateKeyFromCache( acc *account.Account, - correlationID *memguard.LockedBuffer, ) (*memguard.LockedBuffer, error) { - key := CacheKey(correlationID.Bytes()) + cacheKey := cacheKeyPrefix + acc.Nickname + keyHash := KeyHash([]byte(cacheKey)) - value, err := w.gc.Get(key) + value, err := w.gc.Get(keyHash) if err != nil { if err.Error() == gcache.KeyNotFoundError.Error() { - return nil, fmt.Errorf("%w: %w", utils.ErrCorrelationIDNotFound, err) + return nil, fmt.Errorf("%w: %w", utils.ErrPrivateKeyCache, err) } return nil, fmt.Errorf("%w: %w", utils.ErrCache, err) @@ -411,35 +402,33 @@ func (w *walletSign) privateKeyFromCache( cipheredPrivateKey := memguard.NewBufferFromBytes(cacheValue) - privateKey, err := Xor(cipheredPrivateKey, correlationID) + privateKey, err := Xor(cipheredPrivateKey, keyHash) if err != nil { return nil, fmt.Errorf("%w: %w", utils.ErrCache, err) } cipheredPrivateKey.Destroy() - correlationID.Destroy() return privateKey, nil } -func Xor(bufferA, bufferB *memguard.LockedBuffer) (*memguard.LockedBuffer, error) { - a := bufferA.Bytes() - b := bufferB.Bytes() +func Xor(pkey *memguard.LockedBuffer, cacheKeyHash [32]byte) (*memguard.LockedBuffer, error) { + a := pkey.Bytes() - if len(a) != len(b) { - return nil, fmt.Errorf("length of two arrays must be same, %d and %d", len(a), len(b)) + if len(a) != len(cacheKeyHash) { + return nil, fmt.Errorf("length of two arrays must be same, %d and %d", len(a), len(cacheKeyHash)) } result := make([]byte, len(a)) for i := 0; i < len(a); i++ { - result[i] = a[i] ^ b[i] + result[i] = a[i] ^ cacheKeyHash[i] } return memguard.NewBufferFromBytes(result), nil } -func CacheKey(correlationID models.CorrelationID) [32]byte { - return blake3.Sum256(correlationID) +func KeyHash(cacheKeyHash []byte) [32]byte { + return blake3.Sum256(cacheKeyHash) } func expirationDuration() time.Duration { @@ -457,17 +446,6 @@ func expirationDuration() time.Duration { return duration } -func generateCorrelationID() (*memguard.LockedBuffer, error) { - rand := cryptorand.Reader - - correlationId := make([]byte, ed25519.PrivateKeySize) - if _, err := io.ReadFull(rand, correlationId); err != nil { - return nil, err - } - - return memguard.NewBufferFromBytes(correlationId), nil -} - func convertAssetsToModel(assetsWithBalance []*assets.AssetInfoWithBalances) []models.AssetInfo { result := make([]models.AssetInfo, 0) diff --git a/internal/handler/wallet/sign_test.go b/internal/handler/wallet/sign_test.go index 3e2da4dbe..59525db7c 100644 --- a/internal/handler/wallet/sign_test.go +++ b/internal/handler/wallet/sign_test.go @@ -1,15 +1,12 @@ package wallet import ( - "encoding/base64" - "encoding/json" "fmt" "net/http" "net/http/httptest" "strconv" "testing" - "github.com/massalabs/station-massa-wallet/api/server/models" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" "github.com/massalabs/station-massa-wallet/pkg/utils" @@ -72,7 +69,7 @@ func Test_walletSign_Handle(t *testing.T) { // Send password to prompter app and wait for result go func(res chan walletapp.EventData) { prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Password: password, Fees: "14400", } @@ -94,7 +91,7 @@ func Test_walletSign_Handle(t *testing.T) { // Send password to prompter app and wait for result go func(res chan walletapp.EventData) { prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Password: password, Fees: "1000", } @@ -121,7 +118,7 @@ func Test_walletSign_Handle(t *testing.T) { go func(res chan walletapp.EventData) { // Send wrong password to prompter app and wait for result prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Password: "this is not the password", Fees: "1000", } @@ -132,7 +129,7 @@ func Test_walletSign_Handle(t *testing.T) { // Send password to prompter app to unlock the handler prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Password: password, Fees: "1000", } @@ -154,7 +151,7 @@ func Test_walletSign_Handle(t *testing.T) { go func() { // Send wrong password to prompter app and wait for result prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Password: "this is not the password", Fees: "1000", } @@ -170,74 +167,4 @@ func Test_walletSign_Handle(t *testing.T) { resp := signTransaction(t, api, nickname, transactionData) verifyStatusCode(t, resp, http.StatusUnauthorized) }) - - t.Run("sign transaction batch", func(t *testing.T) { - transactionDataBatch := fmt.Sprintf(`{"chainId": `+strconv.FormatUint(ChainIDUnitTests, 10)+`, "operation":"%s","batch":true}`, callSCString) - testResult := make(chan walletapp.EventData) - - // Send password to prompter app and wait for result - go func(res chan walletapp.EventData) { - prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, - Password: password, - Fees: "1000", - } - // forward test result to test goroutine - res <- (<-resChan) - }(testResult) - - resp := signTransaction(t, api, nickname, transactionDataBatch) - verifyStatusCode(t, resp, http.StatusOK) - - result := <-testResult - - checkResultChannel(t, result, true, "") - - var body models.SignResponse - err = json.Unmarshal(resp.Body.Bytes(), &body) - assert.NoError(t, err) - - correlationId := base64.StdEncoding.EncodeToString(body.CorrelationID) - - transactionDataBatch = fmt.Sprintf(`{"chainId": `+strconv.FormatUint(ChainIDUnitTests, 10)+`, "operation":"%s","correlationId":"%s"}`, callSCString, correlationId) - // Send new transaction without password prompt - // --- - // No, we disable batch signing, so send password prompt: - go func(res chan walletapp.EventData) { - prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, - Password: password, - Fees: "1000", - } - // forward test result to test goroutine - res <- (<-resChan) - }(testResult) - // --- - resp = signTransaction(t, api, nickname, transactionDataBatch) - verifyStatusCode(t, resp, http.StatusOK) - - // Send new transaction with incorrect correlation id - correlationId = base64.StdEncoding.EncodeToString([]byte("wrong correlation id")) - transactionDataBatch = fmt.Sprintf(`{"chainId": `+strconv.FormatUint(ChainIDUnitTests, 10)+`, "operation":"%s","correlationId":"%s"}`, callSCString, correlationId) - - // We disable batch signing, so send password prompt: - go func(res chan walletapp.EventData) { - prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, - Password: password, - Fees: "1000", - } - // forward test result to test goroutine - res <- (<-resChan) - }(testResult) - - resp = signTransaction(t, api, nickname, transactionDataBatch) - - verifyStatusCode(t, resp, http.StatusOK) // remove this line when enabling batch signing - // Uncomment the code below when enabling batch signing: - // var bodyError operations.SignInternalServerError - // err = json.Unmarshal(resp.Body.Bytes(), &bodyError) - // assert.NoError(t, err) - // verifyStatusCode(t, resp, http.StatusNotFound) - }) } diff --git a/internal/handler/wallet/testsUtils.go b/internal/handler/wallet/testsUtils.go index 404c2662a..b24d03d0f 100644 --- a/internal/handler/wallet/testsUtils.go +++ b/internal/handler/wallet/testsUtils.go @@ -13,8 +13,7 @@ import ( ) const ( - ChainIDUnitTests = 77658366 // Buildnet - PromptCorrelationTestId = "1" + ChainIDUnitTests = 77658366 // Buildnet ) func createAccount(password, nickname string, t *testing.T, prompterApp prompt.WalletPrompterInterface) *account.Account { diff --git a/internal/handler/wallet/transfer_test.go b/internal/handler/wallet/transfer_test.go index 62e52bad0..04439252c 100644 --- a/internal/handler/wallet/transfer_test.go +++ b/internal/handler/wallet/transfer_test.go @@ -64,7 +64,7 @@ func Test_transfer_handler(t *testing.T) { // Send password to prompter app and wait for result go func(res chan walletapp.EventData) { prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ - BaseMessage: walletapp.BaseMessage{CorrelationID: PromptCorrelationTestId}, + BaseMessage: walletapp.BaseMessage{}, Password: password, Fees: "1000", } diff --git a/pkg/app/app.go b/pkg/app/app.go index 4bea18e2b..74e024937 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -17,7 +17,7 @@ import ( type WalletApp struct { Ctx context.Context CtrlChan chan PromptCtrl - PromptInput chan CorrelationIdentifier + PromptInput chan EventInterface Wallet *wallet.Wallet Shutdown bool IsListening bool @@ -37,7 +37,7 @@ func (a *WalletApp) cleanExit() { func NewWalletApp(wallet *wallet.Wallet) *WalletApp { app := &WalletApp{ CtrlChan: make(chan PromptCtrl), - PromptInput: make(chan CorrelationIdentifier), + PromptInput: make(chan EventInterface), Wallet: wallet, Shutdown: false, IsListening: false, @@ -78,25 +78,25 @@ func (a *WalletApp) BeforeClose(ctx context.Context) bool { // Functions to send user input to the backend from the wails frontend -func (a *WalletApp) SendPromptInput(input string, correlationID string) { +func (a *WalletApp) SendPromptInput(input string) { if !a.IsListening { logger.Warn("Not listening (in SendPromptInput)") return } - a.PromptInput <- &StringPromptInput{BaseMessage: BaseMessage{CorrelationID: correlationID}, Message: input} + a.PromptInput <- &StringPromptInput{BaseMessage: BaseMessage{}, Message: input} } -func (a *WalletApp) SendSignPromptInput(password string, fees string, correlationID string) { +func (a *WalletApp) SendSignPromptInput(password string, fees string) { if !a.IsListening { logger.Warn("Not listening (in SendSignPromptInput)") return } - a.PromptInput <- &SignPromptInput{BaseMessage: BaseMessage{CorrelationID: correlationID}, Password: password, Fees: fees} + a.PromptInput <- &SignPromptInput{BaseMessage: BaseMessage{}, Password: password, Fees: fees} } -func (a *WalletApp) SendPKeyPromptInput(privateKeyText string, nickname string, password string, correlationID string) { +func (a *WalletApp) SendPKeyPromptInput(privateKeyText string, nickname string, password string) { if !a.IsListening { logger.Warn("Not listening (in SendPKeyPromptInput)") return @@ -105,7 +105,7 @@ func (a *WalletApp) SendPKeyPromptInput(privateKeyText string, nickname string, guardedPrivateKey := memguard.NewBufferFromBytes([]byte(privateKeyText)) a.PromptInput <- &ImportPKeyPromptInput{ - BaseMessage: BaseMessage{CorrelationID: correlationID}, + BaseMessage: BaseMessage{}, PrivateKey: guardedPrivateKey, Nickname: nickname, Password: memguard.NewBufferFromBytes([]byte(password)), diff --git a/pkg/app/events.go b/pkg/app/events.go index e898bd13e..a063a3a28 100644 --- a/pkg/app/events.go +++ b/pkg/app/events.go @@ -36,20 +36,12 @@ const ( // User input interfaces for the channel -// CorrelationIdentifier interface that all message types will implement -type CorrelationIdentifier interface { - GetCorrelationID() string -} - -// BaseMessage contains the common CorrelationID attribute -type BaseMessage struct { - CorrelationID string -} +// EventInterface interface that all message types will implement +// Unused for now +type EventInterface interface{} -// GetCorrelationID retrieves the CorrelationID from the BaseMessage struct -func (m *BaseMessage) GetCorrelationID() string { - return m.CorrelationID -} +// BaseMessage contains the common attribute +type BaseMessage struct{} type StringPromptInput struct { BaseMessage diff --git a/pkg/config/config.go b/pkg/config/config.go index 9136b7751..0843a0853 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -2,6 +2,7 @@ package config import ( jsonStd "encoding/json" + "fmt" "log" "os" "path/filepath" @@ -14,17 +15,17 @@ import ( ) type SignRule struct { - Contract string `json:"contract"` - PasswordPrompt bool `json:"passwordPrompt"` - ConfirmationBypass bool `json:"confirmationBypass"` + Contract string `json:"contract"` + PasswordPrompt bool `json:"passwordPrompt"` + AutoSign bool `json:"autoSign"` } -type Account struct { +type AccountCfg struct { SignRules []SignRule `json:"signRules"` } type Config struct { - Accounts map[string]Account `json:"accounts"` + Accounts map[string]AccountCfg `json:"accounts"` } var ( @@ -72,9 +73,20 @@ func Get() *Config { return cfg } +func GetAccountConfig(accountName string) (*AccountCfg, error) { + cfg := Get() + + account, exists := cfg.Accounts[accountName] + if !exists { + return nil, fmt.Errorf("account '%s' not found in configuration", accountName) + } + + return &account, nil +} + func defaultConfig() *Config { return &Config{ - Accounts: map[string]Account{}, + Accounts: map[string]AccountCfg{}, } } diff --git a/pkg/network/operation.go b/pkg/network/operation.go index 9bf52d32b..8419f4923 100644 --- a/pkg/network/operation.go +++ b/pkg/network/operation.go @@ -44,7 +44,6 @@ func SendOperation( operationDataToSign := utils.PrepareSignData(uint64(chainID), append(publicKey, operationData...)) - // TODO: we do not implement the handling of the correlation id for now signature, err := acc.Sign(password, operationDataToSign) if err != nil { return nil, fmt.Errorf("Error while signing operation: %w", err) @@ -62,5 +61,5 @@ func SendOperation( return nil, fmt.Errorf("Error during RPC call: %w", err) } - return &sendOperation.OperationResponse{CorrelationID: "", OperationID: resp[0]}, nil + return &sendOperation.OperationResponse{OperationID: resp[0]}, nil } diff --git a/pkg/prompt/env.go b/pkg/prompt/env.go index daa7647d1..59f0817f3 100644 --- a/pkg/prompt/env.go +++ b/pkg/prompt/env.go @@ -17,9 +17,9 @@ func (e *envPrompter) PromptRequest(req PromptRequest) { switch req.Action { case walletapp.Sign: - e.PromptApp.PromptInput <- &walletapp.SignPromptInput{BaseMessage: walletapp.BaseMessage{CorrelationID: req.CorrelationID}, Password: password, Fees: "10000000"} + e.PromptApp.PromptInput <- &walletapp.SignPromptInput{Password: password, Fees: "10000000"} case walletapp.Delete, walletapp.NewPassword, walletapp.Unprotect: - e.PromptApp.PromptInput <- &walletapp.StringPromptInput{BaseMessage: walletapp.BaseMessage{CorrelationID: req.CorrelationID}, Message: password} + e.PromptApp.PromptInput <- &walletapp.StringPromptInput{Message: password} } }() } diff --git a/pkg/prompt/prompt.go b/pkg/prompt/prompt.go index 4ad1e3cfc..5a1f0674a 100644 --- a/pkg/prompt/prompt.go +++ b/pkg/prompt/prompt.go @@ -3,8 +3,6 @@ package prompt import ( "context" "errors" - "math/rand" - "strconv" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" "github.com/massalabs/station-massa-wallet/pkg/utils" @@ -15,11 +13,10 @@ import ( ) type PromptRequest struct { - Action walletapp.PromptRequestAction - Msg string - Data interface{} - CodeMessage string - CorrelationID string + Action walletapp.PromptRequestAction + Msg string + Data interface{} + CodeMessage string } // WalletPrompter is a struct that wraps a Wails GUI application and implements the WalletPrompterInterface interface. @@ -68,8 +65,6 @@ func WakeUpPrompt( prompterApp.Lock() defer prompterApp.Unlock() - correlationId := strconv.FormatUint(rand.Uint64(), 10) - req.CorrelationID = correlationId prompterApp.PromptRequest(req) ctxTimeout, cancel := context.WithTimeout(context.Background(), TIMEOUT) @@ -80,15 +75,6 @@ func WakeUpPrompt( for { select { case input := <-prompterApp.App().PromptInput: - receivedCorrelationId := input.GetCorrelationID() - // In test environnement we can't provide the correlation id, - // so here we continue only if the correlation id is not the same as the one we sent, - // and if the correlation id is not 1 (which is the test value for correlation id). - if receivedCorrelationId != "1" && receivedCorrelationId != correlationId { - logger.Warn("Received a prompt input with a different correlation id") - continue - } - var keepListening bool var err error diff --git a/pkg/utils/errors.go b/pkg/utils/errors.go index f53edb0e4..1518ef06f 100644 --- a/pkg/utils/errors.go +++ b/pkg/utils/errors.go @@ -34,12 +34,12 @@ const ( // Sentinel errors var ( - ErrCorrelationIDNotFound = errors.New("Correlation ID not found") - ErrCache = errors.New("Error loading cache") - ErrWrongPassword = errors.New("wrong password") - ErrActionCanceled = errors.New("Action canceled by user") - ErrInvalidInputType = errors.New("invalid prompt input type") - ErrTimeout = errors.New("Password prompt reached timeout") + ErrPrivateKeyCache = errors.New("Private key not found in cache") + ErrCache = errors.New("Error loading cache") + ErrWrongPassword = errors.New("wrong password") + ErrActionCanceled = errors.New("Action canceled by user") + ErrInvalidInputType = errors.New("invalid prompt input type") + ErrTimeout = errors.New("Password prompt reached timeout") ) func WailsErrorCode(err error) string { diff --git a/wails-frontend/src/events/events.tsx b/wails-frontend/src/events/events.tsx index 1c3f5111c..6d6b33eb9 100644 --- a/wails-frontend/src/events/events.tsx +++ b/wails-frontend/src/events/events.tsx @@ -20,7 +20,6 @@ export type promptRequest = { Msg: string; Data: any; CodeMessage: string; - CorrelationID: string; }; export const events = { diff --git a/wails-frontend/src/pages/PasswordPromptHandler/Delete.tsx b/wails-frontend/src/pages/PasswordPromptHandler/Delete.tsx index 727678d22..7b2cf8952 100644 --- a/wails-frontend/src/pages/PasswordPromptHandler/Delete.tsx +++ b/wails-frontend/src/pages/PasswordPromptHandler/Delete.tsx @@ -35,7 +35,7 @@ export function Delete() { EventsOnce(events.promptResult, handleResult); - SendPromptInput(password, req.CorrelationID); + SendPromptInput(password); } function handleResult(result: promptResult) { diff --git a/wails-frontend/src/pages/PasswordPromptHandler/Sign.tsx b/wails-frontend/src/pages/PasswordPromptHandler/Sign.tsx index 7a0e94b30..00fe2d704 100644 --- a/wails-frontend/src/pages/PasswordPromptHandler/Sign.tsx +++ b/wails-frontend/src/pages/PasswordPromptHandler/Sign.tsx @@ -98,7 +98,7 @@ export function Sign() { EventsOnce(events.promptResult, handleResult); - SendSignPromptInput(password, fees.toString(), req.CorrelationID); + SendSignPromptInput(password, fees.toString()); } function handleResult(result: promptResult) { diff --git a/wails-frontend/src/pages/backupKeyPairs.tsx b/wails-frontend/src/pages/backupKeyPairs.tsx index f08011225..45758da54 100644 --- a/wails-frontend/src/pages/backupKeyPairs.tsx +++ b/wails-frontend/src/pages/backupKeyPairs.tsx @@ -97,7 +97,7 @@ function BackupKeyPairs() { const { password } = formObject; EventsOnce(events.promptResult, handleResult); - await SendPromptInput(password, req.CorrelationID); + await SendPromptInput(password); } return ( diff --git a/wails-frontend/src/pages/backupMethods.tsx b/wails-frontend/src/pages/backupMethods.tsx index 644723e7e..f740bd4f6 100644 --- a/wails-frontend/src/pages/backupMethods.tsx +++ b/wails-frontend/src/pages/backupMethods.tsx @@ -26,11 +26,11 @@ function BackupMethods() { handleApplyResult(navigate, req, setErrorMsg, true), ); - await SendPromptInput(backupMethods.ymlFileBackup, req.CorrelationID); + await SendPromptInput(backupMethods.ymlFileBackup); } async function handleKeyPairs() { - await SendPromptInput(backupMethods.privateKeyBackup, req.CorrelationID); + await SendPromptInput(backupMethods.privateKeyBackup); navigate('/backup-pkey', { state: { req } }); } diff --git a/wails-frontend/src/pages/confirmDelete.tsx b/wails-frontend/src/pages/confirmDelete.tsx index 0a064d101..04f7ec252 100644 --- a/wails-frontend/src/pages/confirmDelete.tsx +++ b/wails-frontend/src/pages/confirmDelete.tsx @@ -17,7 +17,7 @@ function ConfirmDelete() { function handleConfirm() { EventsOnce(events.promptResult, handleApplyResult(navigate, req)); - SendPromptInput(password, req.CorrelationID); + SendPromptInput(password); } return ( diff --git a/wails-frontend/src/pages/importFile.tsx b/wails-frontend/src/pages/importFile.tsx index 161d31da9..07bef45a6 100644 --- a/wails-frontend/src/pages/importFile.tsx +++ b/wails-frontend/src/pages/importFile.tsx @@ -53,7 +53,7 @@ const ImportFile = () => { events.promptResult, handleApplyResult(nav, req, setErrorMsg, true), ); - await SendPromptInput(account.filePath, req.CorrelationID); + await SendPromptInput(account.filePath); } }; diff --git a/wails-frontend/src/pages/newPassword.tsx b/wails-frontend/src/pages/newPassword.tsx index d02c90987..562eac872 100644 --- a/wails-frontend/src/pages/newPassword.tsx +++ b/wails-frontend/src/pages/newPassword.tsx @@ -88,13 +88,8 @@ function NewPassword() { ); return isImportAction - ? SendPKeyPromptInput( - state.privateKey, - state.nickname, - password, - req.CorrelationID, - ) - : SendPromptInput(password, req.CorrelationID); + ? SendPKeyPromptInput(state.privateKey, state.nickname, password) + : SendPromptInput(password); } async function handleSubmit(e: SyntheticEvent) { diff --git a/wails-frontend/wailsjs/go/walletapp/WalletApp.d.ts b/wails-frontend/wailsjs/go/walletapp/WalletApp.d.ts index e3c59f5b8..b7c5da0a8 100755 --- a/wails-frontend/wailsjs/go/walletapp/WalletApp.d.ts +++ b/wails-frontend/wailsjs/go/walletapp/WalletApp.d.ts @@ -12,10 +12,10 @@ export function IsNicknameValid(arg1:string):Promise; export function SelectAccountFile():Promise; -export function SendPKeyPromptInput(arg1:string,arg2:string,arg3:string,arg4:string):Promise; +export function SendPKeyPromptInput(arg1:string,arg2:string,arg3:string):Promise; -export function SendPromptInput(arg1:string,arg2:string):Promise; +export function SendPromptInput(arg1:string):Promise; -export function SendSignPromptInput(arg1:string,arg2:string,arg3:string):Promise; +export function SendSignPromptInput(arg1:string,arg2:string):Promise; export function Show():Promise; diff --git a/wails-frontend/wailsjs/go/walletapp/WalletApp.js b/wails-frontend/wailsjs/go/walletapp/WalletApp.js index 8ba213d34..19787c800 100755 --- a/wails-frontend/wailsjs/go/walletapp/WalletApp.js +++ b/wails-frontend/wailsjs/go/walletapp/WalletApp.js @@ -22,16 +22,16 @@ export function SelectAccountFile() { return window['go']['walletapp']['WalletApp']['SelectAccountFile'](); } -export function SendPKeyPromptInput(arg1, arg2, arg3, arg4) { - return window['go']['walletapp']['WalletApp']['SendPKeyPromptInput'](arg1, arg2, arg3, arg4); +export function SendPKeyPromptInput(arg1, arg2, arg3) { + return window['go']['walletapp']['WalletApp']['SendPKeyPromptInput'](arg1, arg2, arg3); } -export function SendPromptInput(arg1, arg2) { - return window['go']['walletapp']['WalletApp']['SendPromptInput'](arg1, arg2); +export function SendPromptInput(arg1) { + return window['go']['walletapp']['WalletApp']['SendPromptInput'](arg1); } -export function SendSignPromptInput(arg1, arg2, arg3) { - return window['go']['walletapp']['WalletApp']['SendSignPromptInput'](arg1, arg2, arg3); +export function SendSignPromptInput(arg1, arg2) { + return window['go']['walletapp']['WalletApp']['SendSignPromptInput'](arg1, arg2); } export function Show() { From de6692d022a1108a645f089f48972aa1fdbec12c Mon Sep 17 00:00:00 2001 From: Peterjah Date: Fri, 20 Dec 2024 14:45:57 +0100 Subject: [PATCH 04/10] add getConfig endpoint --- api/server/models/account_config.go | 124 +++++++++++++++ api/server/models/config.go | 111 ++++++++++++++ api/server/models/sign_rule.go | 105 +++++++++++++ api/server/restapi/embedded_spec.go | 142 ++++++++++++++++++ api/server/restapi/operations/get_config.go | 56 +++++++ .../operations/get_config_parameters.go | 46 ++++++ .../operations/get_config_responses.go | 103 +++++++++++++ .../operations/get_config_urlbuilder.go | 84 +++++++++++ .../restapi/operations/massa_wallet_api.go | 12 ++ api/walletApi-V0.yml | 51 +++++++ internal/handler/wallet/api.go | 1 + internal/handler/wallet/errors.go | 1 + internal/handler/wallet/get_config.go | 51 +++++++ 13 files changed, 887 insertions(+) create mode 100644 api/server/models/account_config.go create mode 100644 api/server/models/config.go create mode 100644 api/server/models/sign_rule.go create mode 100644 api/server/restapi/operations/get_config.go create mode 100644 api/server/restapi/operations/get_config_parameters.go create mode 100644 api/server/restapi/operations/get_config_responses.go create mode 100644 api/server/restapi/operations/get_config_urlbuilder.go create mode 100644 internal/handler/wallet/get_config.go diff --git a/api/server/models/account_config.go b/api/server/models/account_config.go new file mode 100644 index 000000000..1442dcb2c --- /dev/null +++ b/api/server/models/account_config.go @@ -0,0 +1,124 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// AccountConfig Wallet Account Config. +// +// swagger:model AccountConfig +type AccountConfig struct { + + // sign rules + // Required: true + SignRules []*SignRule `json:"signRules"` +} + +// Validate validates this account config +func (m *AccountConfig) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateSignRules(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *AccountConfig) validateSignRules(formats strfmt.Registry) error { + + if err := validate.Required("signRules", "body", m.SignRules); err != nil { + return err + } + + for i := 0; i < len(m.SignRules); i++ { + if swag.IsZero(m.SignRules[i]) { // not required + continue + } + + if m.SignRules[i] != nil { + if err := m.SignRules[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("signRules" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("signRules" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this account config based on the context it is used +func (m *AccountConfig) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateSignRules(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *AccountConfig) contextValidateSignRules(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.SignRules); i++ { + + if m.SignRules[i] != nil { + + if swag.IsZero(m.SignRules[i]) { // not required + return nil + } + + if err := m.SignRules[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("signRules" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("signRules" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *AccountConfig) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *AccountConfig) UnmarshalBinary(b []byte) error { + var res AccountConfig + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/api/server/models/config.go b/api/server/models/config.go new file mode 100644 index 000000000..e721cd724 --- /dev/null +++ b/api/server/models/config.go @@ -0,0 +1,111 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// Config Wallet Config. +// +// swagger:model Config +type Config struct { + + // accounts + Accounts map[string]AccountConfig `json:"accounts,omitempty"` +} + +// Validate validates this config +func (m *Config) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAccounts(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *Config) validateAccounts(formats strfmt.Registry) error { + if swag.IsZero(m.Accounts) { // not required + return nil + } + + for k := range m.Accounts { + + if err := validate.Required("accounts"+"."+k, "body", m.Accounts[k]); err != nil { + return err + } + if val, ok := m.Accounts[k]; ok { + if err := val.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("accounts" + "." + k) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("accounts" + "." + k) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this config based on the context it is used +func (m *Config) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateAccounts(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *Config) contextValidateAccounts(ctx context.Context, formats strfmt.Registry) error { + + for k := range m.Accounts { + + if val, ok := m.Accounts[k]; ok { + if err := val.ContextValidate(ctx, formats); err != nil { + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *Config) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *Config) UnmarshalBinary(b []byte) error { + var res Config + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/api/server/models/sign_rule.go b/api/server/models/sign_rule.go new file mode 100644 index 000000000..19cf03d75 --- /dev/null +++ b/api/server/models/sign_rule.go @@ -0,0 +1,105 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// SignRule Account Sign rule. +// +// swagger:model SignRule +type SignRule struct { + + // auto sign + // Required: true + AutoSign *bool `json:"autoSign"` + + // contract + // Required: true + Contract *string `json:"contract"` + + // password prompt + // Required: true + PasswordPrompt *bool `json:"passwordPrompt"` +} + +// Validate validates this sign rule +func (m *SignRule) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAutoSign(formats); err != nil { + res = append(res, err) + } + + if err := m.validateContract(formats); err != nil { + res = append(res, err) + } + + if err := m.validatePasswordPrompt(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *SignRule) validateAutoSign(formats strfmt.Registry) error { + + if err := validate.Required("autoSign", "body", m.AutoSign); err != nil { + return err + } + + return nil +} + +func (m *SignRule) validateContract(formats strfmt.Registry) error { + + if err := validate.Required("contract", "body", m.Contract); err != nil { + return err + } + + return nil +} + +func (m *SignRule) validatePasswordPrompt(formats strfmt.Registry) error { + + if err := validate.Required("passwordPrompt", "body", m.PasswordPrompt); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this sign rule based on context it is used +func (m *SignRule) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *SignRule) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *SignRule) UnmarshalBinary(b []byte) error { + var res SignRule + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/api/server/restapi/embedded_spec.go b/api/server/restapi/embedded_spec.go index c642b353f..76769667e 100644 --- a/api/server/restapi/embedded_spec.go +++ b/api/server/restapi/embedded_spec.go @@ -779,6 +779,29 @@ func init() { } } }, + "/api/config": { + "get": { + "description": "Get wallet config", + "produces": [ + "application/json" + ], + "operationId": "GetConfig", + "responses": { + "200": { + "description": "Config retrieved.", + "schema": { + "$ref": "#/definitions/Config" + } + }, + "500": { + "description": "Internal Server Error - The server has encountered a situation it does not know how to handle.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, "/web-app/{resource}": { "get": { "description": "Route for the ReactJS front-end web application (in /web-frontend)", @@ -851,6 +874,21 @@ func init() { } } }, + "AccountConfig": { + "description": "Wallet Account Config.", + "type": "object", + "required": [ + "signRules" + ], + "properties": { + "signRules": { + "type": "array", + "items": { + "$ref": "#/definitions/SignRule" + } + } + } + }, "Address": { "description": "Account's address.", "type": "string", @@ -904,6 +942,19 @@ func init() { } ] }, + "Config": { + "description": "Wallet Config.", + "type": "object", + "properties": { + "accounts": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/AccountConfig" + } + } + }, + "x-nullable": false + }, "Error": { "description": "Error object.", "type": "object", @@ -1066,6 +1117,26 @@ func init() { } } }, + "SignRule": { + "description": "Account Sign rule.", + "type": "object", + "required": [ + "contract", + "passwordPrompt", + "autoSign" + ], + "properties": { + "autoSign": { + "type": "boolean" + }, + "contract": { + "type": "string" + }, + "passwordPrompt": { + "type": "boolean" + } + } + }, "TransferRequest": { "type": "object", "required": [ @@ -1946,6 +2017,29 @@ func init() { } } }, + "/api/config": { + "get": { + "description": "Get wallet config", + "produces": [ + "application/json" + ], + "operationId": "GetConfig", + "responses": { + "200": { + "description": "Config retrieved.", + "schema": { + "$ref": "#/definitions/Config" + } + }, + "500": { + "description": "Internal Server Error - The server has encountered a situation it does not know how to handle.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, "/web-app/{resource}": { "get": { "description": "Route for the ReactJS front-end web application (in /web-frontend)", @@ -2018,6 +2112,21 @@ func init() { } } }, + "AccountConfig": { + "description": "Wallet Account Config.", + "type": "object", + "required": [ + "signRules" + ], + "properties": { + "signRules": { + "type": "array", + "items": { + "$ref": "#/definitions/SignRule" + } + } + } + }, "Address": { "description": "Account's address.", "type": "string", @@ -2073,6 +2182,19 @@ func init() { } ] }, + "Config": { + "description": "Wallet Config.", + "type": "object", + "properties": { + "accounts": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/AccountConfig" + } + } + }, + "x-nullable": false + }, "Error": { "description": "Error object.", "type": "object", @@ -2235,6 +2357,26 @@ func init() { } } }, + "SignRule": { + "description": "Account Sign rule.", + "type": "object", + "required": [ + "contract", + "passwordPrompt", + "autoSign" + ], + "properties": { + "autoSign": { + "type": "boolean" + }, + "contract": { + "type": "string" + }, + "passwordPrompt": { + "type": "boolean" + } + } + }, "TransferRequest": { "type": "object", "required": [ diff --git a/api/server/restapi/operations/get_config.go b/api/server/restapi/operations/get_config.go new file mode 100644 index 000000000..393016af2 --- /dev/null +++ b/api/server/restapi/operations/get_config.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// GetConfigHandlerFunc turns a function with the right signature into a get config handler +type GetConfigHandlerFunc func(GetConfigParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn GetConfigHandlerFunc) Handle(params GetConfigParams) middleware.Responder { + return fn(params) +} + +// GetConfigHandler interface for that can handle valid get config params +type GetConfigHandler interface { + Handle(GetConfigParams) middleware.Responder +} + +// NewGetConfig creates a new http.Handler for the get config operation +func NewGetConfig(ctx *middleware.Context, handler GetConfigHandler) *GetConfig { + return &GetConfig{Context: ctx, Handler: handler} +} + +/* + GetConfig swagger:route GET /api/config getConfig + +Get wallet config +*/ +type GetConfig struct { + Context *middleware.Context + Handler GetConfigHandler +} + +func (o *GetConfig) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewGetConfigParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/api/server/restapi/operations/get_config_parameters.go b/api/server/restapi/operations/get_config_parameters.go new file mode 100644 index 000000000..7501ecf5e --- /dev/null +++ b/api/server/restapi/operations/get_config_parameters.go @@ -0,0 +1,46 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime/middleware" +) + +// NewGetConfigParams creates a new GetConfigParams object +// +// There are no default values defined in the spec. +func NewGetConfigParams() GetConfigParams { + + return GetConfigParams{} +} + +// GetConfigParams contains all the bound params for the get config operation +// typically these are obtained from a http.Request +// +// swagger:parameters GetConfig +type GetConfigParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewGetConfigParams() beforehand. +func (o *GetConfigParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/api/server/restapi/operations/get_config_responses.go b/api/server/restapi/operations/get_config_responses.go new file mode 100644 index 000000000..c805e7c5d --- /dev/null +++ b/api/server/restapi/operations/get_config_responses.go @@ -0,0 +1,103 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + "github.com/massalabs/station-massa-wallet/api/server/models" +) + +// GetConfigOKCode is the HTTP code returned for type GetConfigOK +const GetConfigOKCode int = 200 + +/* +GetConfigOK Config retrieved. + +swagger:response getConfigOK +*/ +type GetConfigOK struct { + + /* + In: Body + */ + Payload *models.Config `json:"body,omitempty"` +} + +// NewGetConfigOK creates GetConfigOK with default headers values +func NewGetConfigOK() *GetConfigOK { + + return &GetConfigOK{} +} + +// WithPayload adds the payload to the get config o k response +func (o *GetConfigOK) WithPayload(payload *models.Config) *GetConfigOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get config o k response +func (o *GetConfigOK) SetPayload(payload *models.Config) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetConfigOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// GetConfigInternalServerErrorCode is the HTTP code returned for type GetConfigInternalServerError +const GetConfigInternalServerErrorCode int = 500 + +/* +GetConfigInternalServerError Internal Server Error - The server has encountered a situation it does not know how to handle. + +swagger:response getConfigInternalServerError +*/ +type GetConfigInternalServerError struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewGetConfigInternalServerError creates GetConfigInternalServerError with default headers values +func NewGetConfigInternalServerError() *GetConfigInternalServerError { + + return &GetConfigInternalServerError{} +} + +// WithPayload adds the payload to the get config internal server error response +func (o *GetConfigInternalServerError) WithPayload(payload *models.Error) *GetConfigInternalServerError { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get config internal server error response +func (o *GetConfigInternalServerError) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetConfigInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/api/server/restapi/operations/get_config_urlbuilder.go b/api/server/restapi/operations/get_config_urlbuilder.go new file mode 100644 index 000000000..9025f7d48 --- /dev/null +++ b/api/server/restapi/operations/get_config_urlbuilder.go @@ -0,0 +1,84 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" +) + +// GetConfigURL generates an URL for the get config operation +type GetConfigURL struct { + _basePath string +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *GetConfigURL) WithBasePath(bp string) *GetConfigURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *GetConfigURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *GetConfigURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/api/config" + + _basePath := o._basePath + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *GetConfigURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *GetConfigURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *GetConfigURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on GetConfigURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on GetConfigURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *GetConfigURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/api/server/restapi/operations/massa_wallet_api.go b/api/server/restapi/operations/massa_wallet_api.go index c478a5bfd..22e4307bc 100644 --- a/api/server/restapi/operations/massa_wallet_api.go +++ b/api/server/restapi/operations/massa_wallet_api.go @@ -83,6 +83,9 @@ func NewMassaWalletAPI(spec *loads.Document) *MassaWalletAPI { GetAllAssetsHandler: GetAllAssetsHandlerFunc(func(params GetAllAssetsParams) middleware.Responder { return middleware.NotImplemented("operation GetAllAssets has not yet been implemented") }), + GetConfigHandler: GetConfigHandlerFunc(func(params GetConfigParams) middleware.Responder { + return middleware.NotImplemented("operation GetConfig has not yet been implemented") + }), ImportAccountHandler: ImportAccountHandlerFunc(func(params ImportAccountParams) middleware.Responder { return middleware.NotImplemented("operation ImportAccount has not yet been implemented") }), @@ -177,6 +180,8 @@ type MassaWalletAPI struct { GetAccountHandler GetAccountHandler // GetAllAssetsHandler sets the operation handler for the get all assets operation GetAllAssetsHandler GetAllAssetsHandler + // GetConfigHandler sets the operation handler for the get config operation + GetConfigHandler GetConfigHandler // ImportAccountHandler sets the operation handler for the import account operation ImportAccountHandler ImportAccountHandler // SignHandler sets the operation handler for the sign operation @@ -312,6 +317,9 @@ func (o *MassaWalletAPI) Validate() error { if o.GetAllAssetsHandler == nil { unregistered = append(unregistered, "GetAllAssetsHandler") } + if o.GetConfigHandler == nil { + unregistered = append(unregistered, "GetConfigHandler") + } if o.ImportAccountHandler == nil { unregistered = append(unregistered, "ImportAccountHandler") } @@ -472,6 +480,10 @@ func (o *MassaWalletAPI) initHandlerCache() { o.handlers["GET"] = make(map[string]http.Handler) } o.handlers["GET"]["/api/accounts/{nickname}/assets"] = NewGetAllAssets(o.context, o.GetAllAssetsHandler) + if o.handlers["GET"] == nil { + o.handlers["GET"] = make(map[string]http.Handler) + } + o.handlers["GET"]["/api/config"] = NewGetConfig(o.context, o.GetConfigHandler) if o.handlers["PUT"] == nil { o.handlers["PUT"] = make(map[string]http.Handler) } diff --git a/api/walletApi-V0.yml b/api/walletApi-V0.yml index 8906f647c..1eca8438e 100644 --- a/api/walletApi-V0.yml +++ b/api/walletApi-V0.yml @@ -42,6 +42,21 @@ paths: description: Resource not found. schema: $ref: "#/definitions/Error" + /api/config: + get: + operationId: GetConfig + description: Get wallet config + produces: + - application/json + responses: + 200: + description: Config retrieved. + schema: + $ref: "#/definitions/Config" + 500: + description: Internal Server Error - The server has encountered a situation it does not know how to handle. + schema: + $ref: "#/definitions/Error" /api/accounts: get: operationId: AccountList @@ -749,3 +764,39 @@ definitions: type: boolean dollarValue: type: string + + SignRule: + type: object + description: Account Sign rule. + properties: + contract: + type: string + passwordPrompt: + type: boolean + autoSign: + type: boolean + required: + - contract + - passwordPrompt + - autoSign + + AccountConfig: + type: object + description: Wallet Account Config. + properties: + signRules: + type: array + items: + $ref: "#/definitions/SignRule" + required: + - signRules + + Config: + type: object + description: Wallet Config. + properties: + accounts: + type: object + additionalProperties: + $ref: "#/definitions/AccountConfig" + x-nullable: false diff --git a/internal/handler/wallet/api.go b/internal/handler/wallet/api.go index 1207fd441..3fd8c1709 100644 --- a/internal/handler/wallet/api.go +++ b/internal/handler/wallet/api.go @@ -37,6 +37,7 @@ func AppendEndpoints(api *operations.MassaWalletAPI, prompterApp prompt.WalletPr api.AddAssetHandler = NewAddAsset(AssetsStore, massaClient) api.GetAllAssetsHandler = NewGetAllAssets(prompterApp.App().Wallet, AssetsStore, massaClient) api.DeleteAssetHandler = NewDeleteAsset(AssetsStore) + api.GetConfigHandler = NewGetConfig() } // loadAccount loads a wallet from the file system or returns an error. diff --git a/internal/handler/wallet/errors.go b/internal/handler/wallet/errors.go index 664eeebd0..36e710de0 100644 --- a/internal/handler/wallet/errors.go +++ b/internal/handler/wallet/errors.go @@ -31,4 +31,5 @@ const ( errorDeleteAssetJSON errorAssetNotFound errorTradeRoll + internalError ) diff --git a/internal/handler/wallet/get_config.go b/internal/handler/wallet/get_config.go new file mode 100644 index 000000000..952b68321 --- /dev/null +++ b/internal/handler/wallet/get_config.go @@ -0,0 +1,51 @@ +package wallet + +import ( + "github.com/go-openapi/runtime/middleware" + "github.com/massalabs/station-massa-wallet/api/server/models" + "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" + "github.com/massalabs/station-massa-wallet/pkg/config" +) + +func NewGetConfig() operations.GetConfigHandler { + return &getConfig{} +} + +type getConfig struct{} + +func (w *getConfig) Handle(_ operations.GetConfigParams) middleware.Responder { + cfg := config.Get() + modelConfig, err := newConfigModel(cfg) + //nolint:wsl + if err != nil { + return operations.NewGetConfigInternalServerError().WithPayload( + &models.Error{ + Code: internalError, + Message: "Unable to create config model", + }) + } + + return operations.NewGetConfigOK().WithPayload(modelConfig) +} + +func newConfigModel(cfg *config.Config) (*models.Config, error) { + modelAccounts := make(map[string]models.AccountConfig) + + for nickname, accountConfig := range cfg.Accounts { + modelSignRules := make([]*models.SignRule, len(accountConfig.SignRules)) + for i, rule := range accountConfig.SignRules { + modelSignRules[i] = &models.SignRule{ + Contract: &rule.Contract, + PasswordPrompt: &rule.PasswordPrompt, + AutoSign: &rule.AutoSign, + } + } + modelAccounts[nickname] = models.AccountConfig{ + SignRules: modelSignRules, + } + } + + return &models.Config{ + Accounts: modelAccounts, + }, nil +} From 83243c3da3bc71774fb772708cb449e9761d534b Mon Sep 17 00:00:00 2001 From: Peterjah Date: Thu, 9 Jan 2025 13:54:33 +0100 Subject: [PATCH 05/10] add signRule endpoints --- .nvmrc | 1 + api/server/models/add_sign_rule.go | 160 +++++ api/server/models/add_sign_rule_response.go | 71 +++ .../models/delete_sign_rule_response.go | 50 ++ api/server/models/request.go | 126 ++++ api/server/models/request_response.go | 127 ++++ api/server/models/request_type.go | 78 +++ api/server/models/rule_type.go | 78 +++ api/server/models/sign_rule.go | 62 +- api/server/models/update_sign_rule.go | 160 +++++ .../models/update_sign_rule_response.go | 71 +++ api/server/restapi/embedded_spec.go | 599 +++++++++++++++++- .../restapi/operations/add_sign_rule.go | 56 ++ .../operations/add_sign_rule_parameters.go | 108 ++++ .../operations/add_sign_rule_responses.go | 283 +++++++++ .../operations/add_sign_rule_urlbuilder.go | 96 +++ .../restapi/operations/delete_sign_rule.go | 56 ++ .../operations/delete_sign_rule_parameters.go | 95 +++ .../operations/delete_sign_rule_responses.go | 218 +++++++ .../operations/delete_sign_rule_urlbuilder.go | 104 +++ .../restapi/operations/massa_wallet_api.go | 36 ++ api/server/restapi/operations/request.go | 56 ++ .../restapi/operations/request_parameters.go | 108 ++++ .../restapi/operations/request_responses.go | 283 +++++++++ .../restapi/operations/request_urlbuilder.go | 96 +++ .../restapi/operations/update_sign_rule.go | 56 ++ .../operations/update_sign_rule_parameters.go | 132 ++++ .../operations/update_sign_rule_responses.go | 283 +++++++++ .../operations/update_sign_rule_urlbuilder.go | 104 +++ api/walletApi-V0.yml | 206 +++++- cmd/massa-wallet/massa-wallet.go | 10 +- go.mod | 2 +- go.sum | 8 +- internal/handler/api.go | 6 +- internal/handler/wallet/add_assets_test.go | 8 +- internal/handler/wallet/api.go | 22 +- internal/handler/wallet/api_test.go | 33 +- internal/handler/wallet/backup_test.go | 28 +- internal/handler/wallet/cache.go | 155 +++++ internal/handler/wallet/create_test.go | 8 +- internal/handler/wallet/delete_asset_test.go | 4 +- internal/handler/wallet/delete_test.go | 18 +- internal/handler/wallet/errors.go | 5 + internal/handler/wallet/export_test.go | 5 +- .../handler/wallet/get_all_assets_test.go | 6 +- internal/handler/wallet/get_config.go | 6 +- internal/handler/wallet/get_test.go | 16 +- internal/handler/wallet/import_test.go | 14 +- internal/handler/wallet/prompt_utils.go | 45 ++ internal/handler/wallet/rolls.go | 2 +- internal/handler/wallet/rolls_test.go | 8 +- internal/handler/wallet/sign.go | 259 ++------ internal/handler/wallet/sign_message.go | 25 +- internal/handler/wallet/sign_rule_add.go | 110 ++++ internal/handler/wallet/sign_rule_delete.go | 85 +++ internal/handler/wallet/sign_rule_test.go | 107 ++++ internal/handler/wallet/sign_rule_update.go | 103 +++ internal/handler/wallet/sign_test.go | 358 ++++++++++- internal/handler/wallet/transfer.go | 18 +- internal/handler/wallet/transfer_test.go | 6 +- internal/handler/wallet/update_test.go | 6 +- pkg/app/events.go | 11 +- pkg/assets/asset.go | 16 +- pkg/assets/asset_test.go | 20 +- pkg/cache/cache.go | 172 +++++ pkg/config/config.go | 126 ++-- pkg/config/config_test.go | 185 ++++++ pkg/config/rules.go | 261 ++++++++ pkg/config/types.go | 30 + pkg/prompt/prompt.go | 13 +- pkg/prompt/sign.go | 44 +- pkg/types/address.go | 12 + pkg/utils/errors.go | 8 +- web-frontend/src/i18n/en_US.json | 2 +- 74 files changed, 5882 insertions(+), 463 deletions(-) create mode 100644 .nvmrc create mode 100644 api/server/models/add_sign_rule.go create mode 100644 api/server/models/add_sign_rule_response.go create mode 100644 api/server/models/delete_sign_rule_response.go create mode 100644 api/server/models/request.go create mode 100644 api/server/models/request_response.go create mode 100644 api/server/models/request_type.go create mode 100644 api/server/models/rule_type.go create mode 100644 api/server/models/update_sign_rule.go create mode 100644 api/server/models/update_sign_rule_response.go create mode 100644 api/server/restapi/operations/add_sign_rule.go create mode 100644 api/server/restapi/operations/add_sign_rule_parameters.go create mode 100644 api/server/restapi/operations/add_sign_rule_responses.go create mode 100644 api/server/restapi/operations/add_sign_rule_urlbuilder.go create mode 100644 api/server/restapi/operations/delete_sign_rule.go create mode 100644 api/server/restapi/operations/delete_sign_rule_parameters.go create mode 100644 api/server/restapi/operations/delete_sign_rule_responses.go create mode 100644 api/server/restapi/operations/delete_sign_rule_urlbuilder.go create mode 100644 api/server/restapi/operations/request.go create mode 100644 api/server/restapi/operations/request_parameters.go create mode 100644 api/server/restapi/operations/request_responses.go create mode 100644 api/server/restapi/operations/request_urlbuilder.go create mode 100644 api/server/restapi/operations/update_sign_rule.go create mode 100644 api/server/restapi/operations/update_sign_rule_parameters.go create mode 100644 api/server/restapi/operations/update_sign_rule_responses.go create mode 100644 api/server/restapi/operations/update_sign_rule_urlbuilder.go create mode 100644 internal/handler/wallet/cache.go create mode 100644 internal/handler/wallet/prompt_utils.go create mode 100644 internal/handler/wallet/sign_rule_add.go create mode 100644 internal/handler/wallet/sign_rule_delete.go create mode 100644 internal/handler/wallet/sign_rule_test.go create mode 100644 internal/handler/wallet/sign_rule_update.go create mode 100644 pkg/cache/cache.go create mode 100644 pkg/config/config_test.go create mode 100644 pkg/config/rules.go create mode 100644 pkg/config/types.go diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..209e3ef4b --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/api/server/models/add_sign_rule.go b/api/server/models/add_sign_rule.go new file mode 100644 index 000000000..b199f22df --- /dev/null +++ b/api/server/models/add_sign_rule.go @@ -0,0 +1,160 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// AddSignRule add sign rule +// +// swagger:model AddSignRule +type AddSignRule struct { + + // The contract to which the rule applies. + // Required: true + Contract *string `json:"contract"` + + // Description text of what is being done (optional) + // Max Length: 280 + Description string `json:"description,omitempty"` + + // Whether the rule is enabled or not. + // Required: true + Enabled *bool `json:"enabled"` + + // The name of the rule. + Name string `json:"name,omitempty"` + + // rule type + // Required: true + RuleType RuleType `json:"ruleType"` +} + +// Validate validates this add sign rule +func (m *AddSignRule) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateContract(formats); err != nil { + res = append(res, err) + } + + if err := m.validateDescription(formats); err != nil { + res = append(res, err) + } + + if err := m.validateEnabled(formats); err != nil { + res = append(res, err) + } + + if err := m.validateRuleType(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *AddSignRule) validateContract(formats strfmt.Registry) error { + + if err := validate.Required("contract", "body", m.Contract); err != nil { + return err + } + + return nil +} + +func (m *AddSignRule) validateDescription(formats strfmt.Registry) error { + if swag.IsZero(m.Description) { // not required + return nil + } + + if err := validate.MaxLength("description", "body", m.Description, 280); err != nil { + return err + } + + return nil +} + +func (m *AddSignRule) validateEnabled(formats strfmt.Registry) error { + + if err := validate.Required("enabled", "body", m.Enabled); err != nil { + return err + } + + return nil +} + +func (m *AddSignRule) validateRuleType(formats strfmt.Registry) error { + + if err := validate.Required("ruleType", "body", RuleType(m.RuleType)); err != nil { + return err + } + + if err := m.RuleType.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("ruleType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("ruleType") + } + return err + } + + return nil +} + +// ContextValidate validate this add sign rule based on the context it is used +func (m *AddSignRule) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateRuleType(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *AddSignRule) contextValidateRuleType(ctx context.Context, formats strfmt.Registry) error { + + if err := m.RuleType.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("ruleType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("ruleType") + } + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *AddSignRule) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *AddSignRule) UnmarshalBinary(b []byte) error { + var res AddSignRule + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/api/server/models/add_sign_rule_response.go b/api/server/models/add_sign_rule_response.go new file mode 100644 index 000000000..fc1d39a0e --- /dev/null +++ b/api/server/models/add_sign_rule_response.go @@ -0,0 +1,71 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// AddSignRuleResponse add sign rule response +// +// swagger:model AddSignRuleResponse +type AddSignRuleResponse struct { + + // id + // Read Only: true + ID string `json:"id,omitempty"` +} + +// Validate validates this add sign rule response +func (m *AddSignRuleResponse) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validate this add sign rule response based on the context it is used +func (m *AddSignRuleResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateID(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *AddSignRuleResponse) contextValidateID(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "id", "body", string(m.ID)); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *AddSignRuleResponse) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *AddSignRuleResponse) UnmarshalBinary(b []byte) error { + var res AddSignRuleResponse + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/api/server/models/delete_sign_rule_response.go b/api/server/models/delete_sign_rule_response.go new file mode 100644 index 000000000..3b594eae3 --- /dev/null +++ b/api/server/models/delete_sign_rule_response.go @@ -0,0 +1,50 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// DeleteSignRuleResponse delete sign rule response +// +// swagger:model DeleteSignRuleResponse +type DeleteSignRuleResponse struct { + + // Confirmation message for the deletion. + Message string `json:"message,omitempty"` +} + +// Validate validates this delete sign rule response +func (m *DeleteSignRuleResponse) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this delete sign rule response based on context it is used +func (m *DeleteSignRuleResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *DeleteSignRuleResponse) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *DeleteSignRuleResponse) UnmarshalBinary(b []byte) error { + var res DeleteSignRuleResponse + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/api/server/models/request.go b/api/server/models/request.go new file mode 100644 index 000000000..250e43ab9 --- /dev/null +++ b/api/server/models/request.go @@ -0,0 +1,126 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// Request request +// +// swagger:model Request +type Request struct { + + // Arbitrary JSON data associated with the request. + Data interface{} `json:"data,omitempty"` + + // Description text of what is being done (optional) + // Max Length: 280 + Description string `json:"description,omitempty"` + + // request type + // Required: true + RequestType RequestType `json:"requestType"` +} + +// Validate validates this request +func (m *Request) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateDescription(formats); err != nil { + res = append(res, err) + } + + if err := m.validateRequestType(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *Request) validateDescription(formats strfmt.Registry) error { + if swag.IsZero(m.Description) { // not required + return nil + } + + if err := validate.MaxLength("description", "body", m.Description, 280); err != nil { + return err + } + + return nil +} + +func (m *Request) validateRequestType(formats strfmt.Registry) error { + + if err := validate.Required("requestType", "body", RequestType(m.RequestType)); err != nil { + return err + } + + if err := m.RequestType.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("requestType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("requestType") + } + return err + } + + return nil +} + +// ContextValidate validate this request based on the context it is used +func (m *Request) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateRequestType(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *Request) contextValidateRequestType(ctx context.Context, formats strfmt.Registry) error { + + if err := m.RequestType.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("requestType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("requestType") + } + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *Request) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *Request) UnmarshalBinary(b []byte) error { + var res Request + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/api/server/models/request_response.go b/api/server/models/request_response.go new file mode 100644 index 000000000..2bccf88cb --- /dev/null +++ b/api/server/models/request_response.go @@ -0,0 +1,127 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "encoding/json" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// RequestResponse request response +// +// swagger:model RequestResponse +type RequestResponse struct { + + // result + // Read Only: true + // Enum: ["approved","denied","error"] + Result string `json:"result,omitempty"` +} + +// Validate validates this request response +func (m *RequestResponse) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateResult(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +var requestResponseTypeResultPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["approved","denied","error"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + requestResponseTypeResultPropEnum = append(requestResponseTypeResultPropEnum, v) + } +} + +const ( + + // RequestResponseResultApproved captures enum value "approved" + RequestResponseResultApproved string = "approved" + + // RequestResponseResultDenied captures enum value "denied" + RequestResponseResultDenied string = "denied" + + // RequestResponseResultError captures enum value "error" + RequestResponseResultError string = "error" +) + +// prop value enum +func (m *RequestResponse) validateResultEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, requestResponseTypeResultPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *RequestResponse) validateResult(formats strfmt.Registry) error { + if swag.IsZero(m.Result) { // not required + return nil + } + + // value enum + if err := m.validateResultEnum("result", "body", m.Result); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this request response based on the context it is used +func (m *RequestResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateResult(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *RequestResponse) contextValidateResult(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "result", "body", string(m.Result)); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *RequestResponse) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *RequestResponse) UnmarshalBinary(b []byte) error { + var res RequestResponse + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/api/server/models/request_type.go b/api/server/models/request_type.go new file mode 100644 index 000000000..5cf7cf6dc --- /dev/null +++ b/api/server/models/request_type.go @@ -0,0 +1,78 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "encoding/json" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" +) + +// RequestType An enumeration of allowed request types. +// +// swagger:model RequestType +type RequestType string + +func NewRequestType(value RequestType) *RequestType { + return &value +} + +// Pointer returns a pointer to a freshly-allocated RequestType. +func (m RequestType) Pointer() *RequestType { + return &m +} + +const ( + + // RequestTypeAddSignRule captures enum value "addSignRule" + RequestTypeAddSignRule RequestType = "addSignRule" + + // RequestTypeRemoveSignRule captures enum value "removeSignRule" + RequestTypeRemoveSignRule RequestType = "removeSignRule" +) + +// for schema +var requestTypeEnum []interface{} + +func init() { + var res []RequestType + if err := json.Unmarshal([]byte(`["addSignRule","removeSignRule"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + requestTypeEnum = append(requestTypeEnum, v) + } +} + +func (m RequestType) validateRequestTypeEnum(path, location string, value RequestType) error { + if err := validate.EnumCase(path, location, value, requestTypeEnum, true); err != nil { + return err + } + return nil +} + +// Validate validates this request type +func (m RequestType) Validate(formats strfmt.Registry) error { + var res []error + + // value enum + if err := m.validateRequestTypeEnum("", "body", m); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// ContextValidate validates this request type based on context it is used +func (m RequestType) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} diff --git a/api/server/models/rule_type.go b/api/server/models/rule_type.go new file mode 100644 index 000000000..d237b1801 --- /dev/null +++ b/api/server/models/rule_type.go @@ -0,0 +1,78 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "encoding/json" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" +) + +// RuleType An enumeration of the different types of rules. +// +// swagger:model RuleType +type RuleType string + +func NewRuleType(value RuleType) *RuleType { + return &value +} + +// Pointer returns a pointer to a freshly-allocated RuleType. +func (m RuleType) Pointer() *RuleType { + return &m +} + +const ( + + // RuleTypeDisablePasswordPrompt captures enum value "disable_password_prompt" + RuleTypeDisablePasswordPrompt RuleType = "disable_password_prompt" + + // RuleTypeAutoSign captures enum value "auto_sign" + RuleTypeAutoSign RuleType = "auto_sign" +) + +// for schema +var ruleTypeEnum []interface{} + +func init() { + var res []RuleType + if err := json.Unmarshal([]byte(`["disable_password_prompt","auto_sign"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + ruleTypeEnum = append(ruleTypeEnum, v) + } +} + +func (m RuleType) validateRuleTypeEnum(path, location string, value RuleType) error { + if err := validate.EnumCase(path, location, value, ruleTypeEnum, true); err != nil { + return err + } + return nil +} + +// Validate validates this rule type +func (m RuleType) Validate(formats strfmt.Registry) error { + var res []error + + // value enum + if err := m.validateRuleTypeEnum("", "body", m); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// ContextValidate validates this rule type based on context it is used +func (m RuleType) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} diff --git a/api/server/models/sign_rule.go b/api/server/models/sign_rule.go index 19cf03d75..40bc7d779 100644 --- a/api/server/models/sign_rule.go +++ b/api/server/models/sign_rule.go @@ -7,6 +7,7 @@ package models import ( "context" + "encoding/json" "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" @@ -19,32 +20,23 @@ import ( // swagger:model SignRule type SignRule struct { - // auto sign - // Required: true - AutoSign *bool `json:"autoSign"` - // contract - // Required: true - Contract *string `json:"contract"` + Contract *string `json:"contract,omitempty"` - // password prompt + // enabled + Enabled *bool `json:"enabled,omitempty"` + + // rule type // Required: true - PasswordPrompt *bool `json:"passwordPrompt"` + // Enum: ["password_prompt","auto_sign"] + RuleType *string `json:"ruleType"` } // Validate validates this sign rule func (m *SignRule) Validate(formats strfmt.Registry) error { var res []error - if err := m.validateAutoSign(formats); err != nil { - res = append(res, err) - } - - if err := m.validateContract(formats); err != nil { - res = append(res, err) - } - - if err := m.validatePasswordPrompt(formats); err != nil { + if err := m.validateRuleType(formats); err != nil { res = append(res, err) } @@ -54,27 +46,43 @@ func (m *SignRule) Validate(formats strfmt.Registry) error { return nil } -func (m *SignRule) validateAutoSign(formats strfmt.Registry) error { +var signRuleTypeRuleTypePropEnum []interface{} - if err := validate.Required("autoSign", "body", m.AutoSign); err != nil { - return err +func init() { + var res []string + if err := json.Unmarshal([]byte(`["password_prompt","auto_sign"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + signRuleTypeRuleTypePropEnum = append(signRuleTypeRuleTypePropEnum, v) } - - return nil } -func (m *SignRule) validateContract(formats strfmt.Registry) error { +const ( + + // SignRuleRuleTypePasswordPrompt captures enum value "password_prompt" + SignRuleRuleTypePasswordPrompt string = "password_prompt" + + // SignRuleRuleTypeAutoSign captures enum value "auto_sign" + SignRuleRuleTypeAutoSign string = "auto_sign" +) - if err := validate.Required("contract", "body", m.Contract); err != nil { +// prop value enum +func (m *SignRule) validateRuleTypeEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, signRuleTypeRuleTypePropEnum, true); err != nil { return err } - return nil } -func (m *SignRule) validatePasswordPrompt(formats strfmt.Registry) error { +func (m *SignRule) validateRuleType(formats strfmt.Registry) error { + + if err := validate.Required("ruleType", "body", m.RuleType); err != nil { + return err + } - if err := validate.Required("passwordPrompt", "body", m.PasswordPrompt); err != nil { + // value enum + if err := m.validateRuleTypeEnum("ruleType", "body", *m.RuleType); err != nil { return err } diff --git a/api/server/models/update_sign_rule.go b/api/server/models/update_sign_rule.go new file mode 100644 index 000000000..4a7f8353e --- /dev/null +++ b/api/server/models/update_sign_rule.go @@ -0,0 +1,160 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// UpdateSignRule update sign rule +// +// swagger:model UpdateSignRule +type UpdateSignRule struct { + + // The contract to which the rule applies. Use wildcard (*) to apply the rule for contracts. + // Required: true + Contract *string `json:"contract"` + + // Description text of what is being updated (optional) + // Max Length: 280 + Description string `json:"description,omitempty"` + + // Whether the rule is enabled or not. + // Required: true + Enabled *bool `json:"enabled"` + + // The name of the rule. + Name string `json:"name,omitempty"` + + // rule type + // Required: true + RuleType RuleType `json:"ruleType"` +} + +// Validate validates this update sign rule +func (m *UpdateSignRule) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateContract(formats); err != nil { + res = append(res, err) + } + + if err := m.validateDescription(formats); err != nil { + res = append(res, err) + } + + if err := m.validateEnabled(formats); err != nil { + res = append(res, err) + } + + if err := m.validateRuleType(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *UpdateSignRule) validateContract(formats strfmt.Registry) error { + + if err := validate.Required("contract", "body", m.Contract); err != nil { + return err + } + + return nil +} + +func (m *UpdateSignRule) validateDescription(formats strfmt.Registry) error { + if swag.IsZero(m.Description) { // not required + return nil + } + + if err := validate.MaxLength("description", "body", m.Description, 280); err != nil { + return err + } + + return nil +} + +func (m *UpdateSignRule) validateEnabled(formats strfmt.Registry) error { + + if err := validate.Required("enabled", "body", m.Enabled); err != nil { + return err + } + + return nil +} + +func (m *UpdateSignRule) validateRuleType(formats strfmt.Registry) error { + + if err := validate.Required("ruleType", "body", RuleType(m.RuleType)); err != nil { + return err + } + + if err := m.RuleType.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("ruleType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("ruleType") + } + return err + } + + return nil +} + +// ContextValidate validate this update sign rule based on the context it is used +func (m *UpdateSignRule) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateRuleType(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *UpdateSignRule) contextValidateRuleType(ctx context.Context, formats strfmt.Registry) error { + + if err := m.RuleType.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("ruleType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("ruleType") + } + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *UpdateSignRule) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *UpdateSignRule) UnmarshalBinary(b []byte) error { + var res UpdateSignRule + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/api/server/models/update_sign_rule_response.go b/api/server/models/update_sign_rule_response.go new file mode 100644 index 000000000..de9effc58 --- /dev/null +++ b/api/server/models/update_sign_rule_response.go @@ -0,0 +1,71 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// UpdateSignRuleResponse update sign rule response +// +// swagger:model UpdateSignRuleResponse +type UpdateSignRuleResponse struct { + + // id + // Read Only: true + ID string `json:"id,omitempty"` +} + +// Validate validates this update sign rule response +func (m *UpdateSignRuleResponse) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validate this update sign rule response based on the context it is used +func (m *UpdateSignRuleResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateID(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *UpdateSignRuleResponse) contextValidateID(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "id", "body", string(m.ID)); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *UpdateSignRuleResponse) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *UpdateSignRuleResponse) UnmarshalBinary(b []byte) error { + var res UpdateSignRuleResponse + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/api/server/restapi/embedded_spec.go b/api/server/restapi/embedded_spec.go index 76769667e..7620731dc 100644 --- a/api/server/restapi/embedded_spec.go +++ b/api/server/restapi/embedded_spec.go @@ -175,7 +175,6 @@ func init() { "$ref": "#/parameters/nickname" }, { - "x-nullable": false, "name": "body", "in": "body", "required": true, @@ -718,6 +717,173 @@ func init() { } } }, + "/api/accounts/{nickname}/signrules": { + "post": { + "description": "Create a new sign rule for the account associated with the provided nickname.", + "produces": [ + "application/json" + ], + "operationId": "AddSignRule", + "parameters": [ + { + "$ref": "#/parameters/nickname" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AddSignRule" + } + } + ], + "responses": { + "200": { + "description": "signRule Id.", + "schema": { + "$ref": "#/definitions/AddSignRuleResponse" + } + }, + "400": { + "description": "Bad request.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized - The request requires user authentication.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Account Not found.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Unprocessable Entity - syntax is correct, but the server was unable to process the contained instructions.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error - The server has encountered a situation it does not know how to handle.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/api/accounts/{nickname}/signrules/{ruleId}": { + "put": { + "description": "Update an existing sign rule for the account associated with the provided nickname by its ID.", + "produces": [ + "application/json" + ], + "operationId": "UpdateSignRule", + "parameters": [ + { + "$ref": "#/parameters/nickname" + }, + { + "$ref": "#/parameters/ruleId" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateSignRule" + } + } + ], + "responses": { + "200": { + "description": "Sign rule updated successfully.", + "schema": { + "$ref": "#/definitions/UpdateSignRuleResponse" + } + }, + "400": { + "description": "Bad request.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized - The request requires user authentication.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Account or Rule Not found.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Unprocessable Entity - syntax is correct, but the server was unable to process the contained instructions.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error - The server has encountered a situation it does not know how to handle.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "delete": { + "description": "Delete a sign rule for the account associated with the provided nickname by its ID.", + "produces": [ + "application/json" + ], + "operationId": "DeleteSignRule", + "parameters": [ + { + "$ref": "#/parameters/nickname" + }, + { + "$ref": "#/parameters/ruleId" + } + ], + "responses": { + "200": { + "description": "Sign rule deleted successfully." + }, + "400": { + "description": "Bad request.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized - The request requires user authentication.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Account or Rule Not found.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error - The server has encountered a situation it does not know how to handle.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, "/api/accounts/{nickname}/transfer": { "post": { "description": "Transfer coins from the account associated with the provided nickname in the path. Will ask the user to enter its account password.", @@ -889,6 +1055,46 @@ func init() { } } }, + "AddSignRule": { + "type": "object", + "required": [ + "ruleType", + "contract", + "enabled" + ], + "properties": { + "contract": { + "description": "The contract to which the rule applies.", + "type": "string" + }, + "description": { + "description": "Description text of what is being done (optional)", + "type": "string", + "maxLength": 280 + }, + "enabled": { + "description": "Whether the rule is enabled or not.", + "type": "boolean" + }, + "name": { + "description": "The name of the rule.", + "type": "string" + }, + "ruleType": { + "$ref": "#/definitions/RuleType" + } + } + }, + "AddSignRuleResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "x-nullable": false, + "readOnly": true + } + } + }, "Address": { "description": "Account's address.", "type": "string", @@ -1049,6 +1255,15 @@ func init() { } } }, + "RuleType": { + "description": "An enumeration of the different types of rules.", + "type": "string", + "enum": [ + "disable_password_prompt", + "auto_sign" + ], + "x-nullable": false + }, "SignMessageRequest": { "type": "object", "properties": { @@ -1121,19 +1336,23 @@ func init() { "description": "Account Sign rule.", "type": "object", "required": [ - "contract", - "passwordPrompt", - "autoSign" + "ruleType" ], "properties": { - "autoSign": { - "type": "boolean" - }, "contract": { - "type": "string" + "type": "string", + "x-nullable": true }, - "passwordPrompt": { - "type": "boolean" + "enabled": { + "type": "boolean", + "default": true + }, + "ruleType": { + "type": "string", + "enum": [ + "password_prompt", + "auto_sign" + ] } } }, @@ -1179,6 +1398,46 @@ func init() { "$ref": "#/definitions/Nickname" } } + }, + "UpdateSignRule": { + "type": "object", + "required": [ + "ruleType", + "contract", + "enabled" + ], + "properties": { + "contract": { + "description": "The contract to which the rule applies. Use wildcard (*) to apply the rule for contracts.", + "type": "string" + }, + "description": { + "description": "Description text of what is being updated (optional)", + "type": "string", + "maxLength": 280 + }, + "enabled": { + "description": "Whether the rule is enabled or not.", + "type": "boolean" + }, + "name": { + "description": "The name of the rule.", + "type": "string" + }, + "ruleType": { + "$ref": "#/definitions/RuleType" + } + } + }, + "UpdateSignRuleResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "x-nullable": false, + "readOnly": true + } + } } }, "parameters": { @@ -1197,6 +1456,14 @@ func init() { "name": "nickname", "in": "path", "required": true + }, + "ruleId": { + "type": "string", + "x-nullable": false, + "description": "The ID of the sign rule", + "name": "ruleId", + "in": "path", + "required": true } } }`)) @@ -1373,7 +1640,6 @@ func init() { "required": true }, { - "x-nullable": false, "name": "body", "in": "body", "required": true, @@ -1951,6 +2217,198 @@ func init() { } } }, + "/api/accounts/{nickname}/signrules": { + "post": { + "description": "Create a new sign rule for the account associated with the provided nickname.", + "produces": [ + "application/json" + ], + "operationId": "AddSignRule", + "parameters": [ + { + "type": "string", + "x-nullable": false, + "description": "Account's short name.", + "name": "nickname", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AddSignRule" + } + } + ], + "responses": { + "200": { + "description": "signRule Id.", + "schema": { + "$ref": "#/definitions/AddSignRuleResponse" + } + }, + "400": { + "description": "Bad request.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized - The request requires user authentication.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Account Not found.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Unprocessable Entity - syntax is correct, but the server was unable to process the contained instructions.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error - The server has encountered a situation it does not know how to handle.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/api/accounts/{nickname}/signrules/{ruleId}": { + "put": { + "description": "Update an existing sign rule for the account associated with the provided nickname by its ID.", + "produces": [ + "application/json" + ], + "operationId": "UpdateSignRule", + "parameters": [ + { + "type": "string", + "x-nullable": false, + "description": "Account's short name.", + "name": "nickname", + "in": "path", + "required": true + }, + { + "type": "string", + "x-nullable": false, + "description": "The ID of the sign rule", + "name": "ruleId", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateSignRule" + } + } + ], + "responses": { + "200": { + "description": "Sign rule updated successfully.", + "schema": { + "$ref": "#/definitions/UpdateSignRuleResponse" + } + }, + "400": { + "description": "Bad request.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized - The request requires user authentication.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Account or Rule Not found.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Unprocessable Entity - syntax is correct, but the server was unable to process the contained instructions.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error - The server has encountered a situation it does not know how to handle.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "delete": { + "description": "Delete a sign rule for the account associated with the provided nickname by its ID.", + "produces": [ + "application/json" + ], + "operationId": "DeleteSignRule", + "parameters": [ + { + "type": "string", + "x-nullable": false, + "description": "Account's short name.", + "name": "nickname", + "in": "path", + "required": true + }, + { + "type": "string", + "x-nullable": false, + "description": "The ID of the sign rule", + "name": "ruleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Sign rule deleted successfully." + }, + "400": { + "description": "Bad request.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized - The request requires user authentication.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Account or Rule Not found.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error - The server has encountered a situation it does not know how to handle.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, "/api/accounts/{nickname}/transfer": { "post": { "description": "Transfer coins from the account associated with the provided nickname in the path. Will ask the user to enter its account password.", @@ -2127,6 +2585,46 @@ func init() { } } }, + "AddSignRule": { + "type": "object", + "required": [ + "ruleType", + "contract", + "enabled" + ], + "properties": { + "contract": { + "description": "The contract to which the rule applies.", + "type": "string" + }, + "description": { + "description": "Description text of what is being done (optional)", + "type": "string", + "maxLength": 280 + }, + "enabled": { + "description": "Whether the rule is enabled or not.", + "type": "boolean" + }, + "name": { + "description": "The name of the rule.", + "type": "string" + }, + "ruleType": { + "$ref": "#/definitions/RuleType" + } + } + }, + "AddSignRuleResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "x-nullable": false, + "readOnly": true + } + } + }, "Address": { "description": "Account's address.", "type": "string", @@ -2289,6 +2787,15 @@ func init() { } } }, + "RuleType": { + "description": "An enumeration of the different types of rules.", + "type": "string", + "enum": [ + "disable_password_prompt", + "auto_sign" + ], + "x-nullable": false + }, "SignMessageRequest": { "type": "object", "properties": { @@ -2361,19 +2868,23 @@ func init() { "description": "Account Sign rule.", "type": "object", "required": [ - "contract", - "passwordPrompt", - "autoSign" + "ruleType" ], "properties": { - "autoSign": { - "type": "boolean" - }, "contract": { - "type": "string" + "type": "string", + "x-nullable": true }, - "passwordPrompt": { - "type": "boolean" + "enabled": { + "type": "boolean", + "default": true + }, + "ruleType": { + "type": "string", + "enum": [ + "password_prompt", + "auto_sign" + ] } } }, @@ -2419,6 +2930,46 @@ func init() { "$ref": "#/definitions/Nickname" } } + }, + "UpdateSignRule": { + "type": "object", + "required": [ + "ruleType", + "contract", + "enabled" + ], + "properties": { + "contract": { + "description": "The contract to which the rule applies. Use wildcard (*) to apply the rule for contracts.", + "type": "string" + }, + "description": { + "description": "Description text of what is being updated (optional)", + "type": "string", + "maxLength": 280 + }, + "enabled": { + "description": "Whether the rule is enabled or not.", + "type": "boolean" + }, + "name": { + "description": "The name of the rule.", + "type": "string" + }, + "ruleType": { + "$ref": "#/definitions/RuleType" + } + } + }, + "UpdateSignRuleResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "x-nullable": false, + "readOnly": true + } + } } }, "parameters": { @@ -2437,6 +2988,14 @@ func init() { "name": "nickname", "in": "path", "required": true + }, + "ruleId": { + "type": "string", + "x-nullable": false, + "description": "The ID of the sign rule", + "name": "ruleId", + "in": "path", + "required": true } } }`)) diff --git a/api/server/restapi/operations/add_sign_rule.go b/api/server/restapi/operations/add_sign_rule.go new file mode 100644 index 000000000..f19e22e19 --- /dev/null +++ b/api/server/restapi/operations/add_sign_rule.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// AddSignRuleHandlerFunc turns a function with the right signature into a add sign rule handler +type AddSignRuleHandlerFunc func(AddSignRuleParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn AddSignRuleHandlerFunc) Handle(params AddSignRuleParams) middleware.Responder { + return fn(params) +} + +// AddSignRuleHandler interface for that can handle valid add sign rule params +type AddSignRuleHandler interface { + Handle(AddSignRuleParams) middleware.Responder +} + +// NewAddSignRule creates a new http.Handler for the add sign rule operation +func NewAddSignRule(ctx *middleware.Context, handler AddSignRuleHandler) *AddSignRule { + return &AddSignRule{Context: ctx, Handler: handler} +} + +/* + AddSignRule swagger:route POST /api/accounts/{nickname}/signrules addSignRule + +Create a new sign rule for the account associated with the provided nickname. +*/ +type AddSignRule struct { + Context *middleware.Context + Handler AddSignRuleHandler +} + +func (o *AddSignRule) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewAddSignRuleParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/api/server/restapi/operations/add_sign_rule_parameters.go b/api/server/restapi/operations/add_sign_rule_parameters.go new file mode 100644 index 000000000..d13af654d --- /dev/null +++ b/api/server/restapi/operations/add_sign_rule_parameters.go @@ -0,0 +1,108 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "io" + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" + "github.com/massalabs/station-massa-wallet/api/server/models" +) + +// NewAddSignRuleParams creates a new AddSignRuleParams object +// +// There are no default values defined in the spec. +func NewAddSignRuleParams() AddSignRuleParams { + + return AddSignRuleParams{} +} + +// AddSignRuleParams contains all the bound params for the add sign rule operation +// typically these are obtained from a http.Request +// +// swagger:parameters AddSignRule +type AddSignRuleParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: body + */ + Body *models.AddSignRule + /*Account's short name. + Required: true + In: path + */ + Nickname string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewAddSignRuleParams() beforehand. +func (o *AddSignRuleParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body models.AddSignRule + if err := route.Consumer.Consume(r.Body, &body); err != nil { + if err == io.EOF { + res = append(res, errors.Required("body", "body", "")) + } else { + res = append(res, errors.NewParseError("body", "body", "", err)) + } + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + ctx := validate.WithOperationRequest(r.Context()) + if err := body.ContextValidate(ctx, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.Body = &body + } + } + } else { + res = append(res, errors.Required("body", "body", "")) + } + + rNickname, rhkNickname, _ := route.Params.GetOK("nickname") + if err := o.bindNickname(rNickname, rhkNickname, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindNickname binds and validates parameter Nickname from path. +func (o *AddSignRuleParams) bindNickname(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.Nickname = raw + + return nil +} diff --git a/api/server/restapi/operations/add_sign_rule_responses.go b/api/server/restapi/operations/add_sign_rule_responses.go new file mode 100644 index 000000000..92bf4a8a8 --- /dev/null +++ b/api/server/restapi/operations/add_sign_rule_responses.go @@ -0,0 +1,283 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + "github.com/massalabs/station-massa-wallet/api/server/models" +) + +// AddSignRuleOKCode is the HTTP code returned for type AddSignRuleOK +const AddSignRuleOKCode int = 200 + +/* +AddSignRuleOK signRule Id. + +swagger:response addSignRuleOK +*/ +type AddSignRuleOK struct { + + /* + In: Body + */ + Payload *models.AddSignRuleResponse `json:"body,omitempty"` +} + +// NewAddSignRuleOK creates AddSignRuleOK with default headers values +func NewAddSignRuleOK() *AddSignRuleOK { + + return &AddSignRuleOK{} +} + +// WithPayload adds the payload to the add sign rule o k response +func (o *AddSignRuleOK) WithPayload(payload *models.AddSignRuleResponse) *AddSignRuleOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the add sign rule o k response +func (o *AddSignRuleOK) SetPayload(payload *models.AddSignRuleResponse) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *AddSignRuleOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// AddSignRuleBadRequestCode is the HTTP code returned for type AddSignRuleBadRequest +const AddSignRuleBadRequestCode int = 400 + +/* +AddSignRuleBadRequest Bad request. + +swagger:response addSignRuleBadRequest +*/ +type AddSignRuleBadRequest struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewAddSignRuleBadRequest creates AddSignRuleBadRequest with default headers values +func NewAddSignRuleBadRequest() *AddSignRuleBadRequest { + + return &AddSignRuleBadRequest{} +} + +// WithPayload adds the payload to the add sign rule bad request response +func (o *AddSignRuleBadRequest) WithPayload(payload *models.Error) *AddSignRuleBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the add sign rule bad request response +func (o *AddSignRuleBadRequest) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *AddSignRuleBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// AddSignRuleUnauthorizedCode is the HTTP code returned for type AddSignRuleUnauthorized +const AddSignRuleUnauthorizedCode int = 401 + +/* +AddSignRuleUnauthorized Unauthorized - The request requires user authentication. + +swagger:response addSignRuleUnauthorized +*/ +type AddSignRuleUnauthorized struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewAddSignRuleUnauthorized creates AddSignRuleUnauthorized with default headers values +func NewAddSignRuleUnauthorized() *AddSignRuleUnauthorized { + + return &AddSignRuleUnauthorized{} +} + +// WithPayload adds the payload to the add sign rule unauthorized response +func (o *AddSignRuleUnauthorized) WithPayload(payload *models.Error) *AddSignRuleUnauthorized { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the add sign rule unauthorized response +func (o *AddSignRuleUnauthorized) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *AddSignRuleUnauthorized) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(401) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// AddSignRuleNotFoundCode is the HTTP code returned for type AddSignRuleNotFound +const AddSignRuleNotFoundCode int = 404 + +/* +AddSignRuleNotFound Account Not found. + +swagger:response addSignRuleNotFound +*/ +type AddSignRuleNotFound struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewAddSignRuleNotFound creates AddSignRuleNotFound with default headers values +func NewAddSignRuleNotFound() *AddSignRuleNotFound { + + return &AddSignRuleNotFound{} +} + +// WithPayload adds the payload to the add sign rule not found response +func (o *AddSignRuleNotFound) WithPayload(payload *models.Error) *AddSignRuleNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the add sign rule not found response +func (o *AddSignRuleNotFound) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *AddSignRuleNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(404) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// AddSignRuleUnprocessableEntityCode is the HTTP code returned for type AddSignRuleUnprocessableEntity +const AddSignRuleUnprocessableEntityCode int = 422 + +/* +AddSignRuleUnprocessableEntity Unprocessable Entity - syntax is correct, but the server was unable to process the contained instructions. + +swagger:response addSignRuleUnprocessableEntity +*/ +type AddSignRuleUnprocessableEntity struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewAddSignRuleUnprocessableEntity creates AddSignRuleUnprocessableEntity with default headers values +func NewAddSignRuleUnprocessableEntity() *AddSignRuleUnprocessableEntity { + + return &AddSignRuleUnprocessableEntity{} +} + +// WithPayload adds the payload to the add sign rule unprocessable entity response +func (o *AddSignRuleUnprocessableEntity) WithPayload(payload *models.Error) *AddSignRuleUnprocessableEntity { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the add sign rule unprocessable entity response +func (o *AddSignRuleUnprocessableEntity) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *AddSignRuleUnprocessableEntity) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(422) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// AddSignRuleInternalServerErrorCode is the HTTP code returned for type AddSignRuleInternalServerError +const AddSignRuleInternalServerErrorCode int = 500 + +/* +AddSignRuleInternalServerError Internal Server Error - The server has encountered a situation it does not know how to handle. + +swagger:response addSignRuleInternalServerError +*/ +type AddSignRuleInternalServerError struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewAddSignRuleInternalServerError creates AddSignRuleInternalServerError with default headers values +func NewAddSignRuleInternalServerError() *AddSignRuleInternalServerError { + + return &AddSignRuleInternalServerError{} +} + +// WithPayload adds the payload to the add sign rule internal server error response +func (o *AddSignRuleInternalServerError) WithPayload(payload *models.Error) *AddSignRuleInternalServerError { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the add sign rule internal server error response +func (o *AddSignRuleInternalServerError) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *AddSignRuleInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/api/server/restapi/operations/add_sign_rule_urlbuilder.go b/api/server/restapi/operations/add_sign_rule_urlbuilder.go new file mode 100644 index 000000000..a0244b8c4 --- /dev/null +++ b/api/server/restapi/operations/add_sign_rule_urlbuilder.go @@ -0,0 +1,96 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// AddSignRuleURL generates an URL for the add sign rule operation +type AddSignRuleURL struct { + Nickname string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *AddSignRuleURL) WithBasePath(bp string) *AddSignRuleURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *AddSignRuleURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *AddSignRuleURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/api/accounts/{nickname}/signrules" + + nickname := o.Nickname + if nickname != "" { + _path = strings.Replace(_path, "{nickname}", nickname, -1) + } else { + return nil, errors.New("nickname is required on AddSignRuleURL") + } + + _basePath := o._basePath + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *AddSignRuleURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *AddSignRuleURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *AddSignRuleURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on AddSignRuleURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on AddSignRuleURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *AddSignRuleURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/api/server/restapi/operations/delete_sign_rule.go b/api/server/restapi/operations/delete_sign_rule.go new file mode 100644 index 000000000..cb8fbb793 --- /dev/null +++ b/api/server/restapi/operations/delete_sign_rule.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// DeleteSignRuleHandlerFunc turns a function with the right signature into a delete sign rule handler +type DeleteSignRuleHandlerFunc func(DeleteSignRuleParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn DeleteSignRuleHandlerFunc) Handle(params DeleteSignRuleParams) middleware.Responder { + return fn(params) +} + +// DeleteSignRuleHandler interface for that can handle valid delete sign rule params +type DeleteSignRuleHandler interface { + Handle(DeleteSignRuleParams) middleware.Responder +} + +// NewDeleteSignRule creates a new http.Handler for the delete sign rule operation +func NewDeleteSignRule(ctx *middleware.Context, handler DeleteSignRuleHandler) *DeleteSignRule { + return &DeleteSignRule{Context: ctx, Handler: handler} +} + +/* + DeleteSignRule swagger:route DELETE /api/accounts/{nickname}/signrules/{ruleId} deleteSignRule + +Delete a sign rule for the account associated with the provided nickname by its ID. +*/ +type DeleteSignRule struct { + Context *middleware.Context + Handler DeleteSignRuleHandler +} + +func (o *DeleteSignRule) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewDeleteSignRuleParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/api/server/restapi/operations/delete_sign_rule_parameters.go b/api/server/restapi/operations/delete_sign_rule_parameters.go new file mode 100644 index 000000000..8dda7e43f --- /dev/null +++ b/api/server/restapi/operations/delete_sign_rule_parameters.go @@ -0,0 +1,95 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" +) + +// NewDeleteSignRuleParams creates a new DeleteSignRuleParams object +// +// There are no default values defined in the spec. +func NewDeleteSignRuleParams() DeleteSignRuleParams { + + return DeleteSignRuleParams{} +} + +// DeleteSignRuleParams contains all the bound params for the delete sign rule operation +// typically these are obtained from a http.Request +// +// swagger:parameters DeleteSignRule +type DeleteSignRuleParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /*Account's short name. + Required: true + In: path + */ + Nickname string + /*The ID of the sign rule + Required: true + In: path + */ + RuleID string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewDeleteSignRuleParams() beforehand. +func (o *DeleteSignRuleParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + rNickname, rhkNickname, _ := route.Params.GetOK("nickname") + if err := o.bindNickname(rNickname, rhkNickname, route.Formats); err != nil { + res = append(res, err) + } + + rRuleID, rhkRuleID, _ := route.Params.GetOK("ruleId") + if err := o.bindRuleID(rRuleID, rhkRuleID, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindNickname binds and validates parameter Nickname from path. +func (o *DeleteSignRuleParams) bindNickname(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.Nickname = raw + + return nil +} + +// bindRuleID binds and validates parameter RuleID from path. +func (o *DeleteSignRuleParams) bindRuleID(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.RuleID = raw + + return nil +} diff --git a/api/server/restapi/operations/delete_sign_rule_responses.go b/api/server/restapi/operations/delete_sign_rule_responses.go new file mode 100644 index 000000000..3535ba75f --- /dev/null +++ b/api/server/restapi/operations/delete_sign_rule_responses.go @@ -0,0 +1,218 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + "github.com/massalabs/station-massa-wallet/api/server/models" +) + +// DeleteSignRuleOKCode is the HTTP code returned for type DeleteSignRuleOK +const DeleteSignRuleOKCode int = 200 + +/* +DeleteSignRuleOK Sign rule deleted successfully. + +swagger:response deleteSignRuleOK +*/ +type DeleteSignRuleOK struct { +} + +// NewDeleteSignRuleOK creates DeleteSignRuleOK with default headers values +func NewDeleteSignRuleOK() *DeleteSignRuleOK { + + return &DeleteSignRuleOK{} +} + +// WriteResponse to the client +func (o *DeleteSignRuleOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(200) +} + +// DeleteSignRuleBadRequestCode is the HTTP code returned for type DeleteSignRuleBadRequest +const DeleteSignRuleBadRequestCode int = 400 + +/* +DeleteSignRuleBadRequest Bad request. + +swagger:response deleteSignRuleBadRequest +*/ +type DeleteSignRuleBadRequest struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewDeleteSignRuleBadRequest creates DeleteSignRuleBadRequest with default headers values +func NewDeleteSignRuleBadRequest() *DeleteSignRuleBadRequest { + + return &DeleteSignRuleBadRequest{} +} + +// WithPayload adds the payload to the delete sign rule bad request response +func (o *DeleteSignRuleBadRequest) WithPayload(payload *models.Error) *DeleteSignRuleBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the delete sign rule bad request response +func (o *DeleteSignRuleBadRequest) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DeleteSignRuleBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// DeleteSignRuleUnauthorizedCode is the HTTP code returned for type DeleteSignRuleUnauthorized +const DeleteSignRuleUnauthorizedCode int = 401 + +/* +DeleteSignRuleUnauthorized Unauthorized - The request requires user authentication. + +swagger:response deleteSignRuleUnauthorized +*/ +type DeleteSignRuleUnauthorized struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewDeleteSignRuleUnauthorized creates DeleteSignRuleUnauthorized with default headers values +func NewDeleteSignRuleUnauthorized() *DeleteSignRuleUnauthorized { + + return &DeleteSignRuleUnauthorized{} +} + +// WithPayload adds the payload to the delete sign rule unauthorized response +func (o *DeleteSignRuleUnauthorized) WithPayload(payload *models.Error) *DeleteSignRuleUnauthorized { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the delete sign rule unauthorized response +func (o *DeleteSignRuleUnauthorized) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DeleteSignRuleUnauthorized) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(401) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// DeleteSignRuleNotFoundCode is the HTTP code returned for type DeleteSignRuleNotFound +const DeleteSignRuleNotFoundCode int = 404 + +/* +DeleteSignRuleNotFound Account or Rule Not found. + +swagger:response deleteSignRuleNotFound +*/ +type DeleteSignRuleNotFound struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewDeleteSignRuleNotFound creates DeleteSignRuleNotFound with default headers values +func NewDeleteSignRuleNotFound() *DeleteSignRuleNotFound { + + return &DeleteSignRuleNotFound{} +} + +// WithPayload adds the payload to the delete sign rule not found response +func (o *DeleteSignRuleNotFound) WithPayload(payload *models.Error) *DeleteSignRuleNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the delete sign rule not found response +func (o *DeleteSignRuleNotFound) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DeleteSignRuleNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(404) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// DeleteSignRuleInternalServerErrorCode is the HTTP code returned for type DeleteSignRuleInternalServerError +const DeleteSignRuleInternalServerErrorCode int = 500 + +/* +DeleteSignRuleInternalServerError Internal Server Error - The server has encountered a situation it does not know how to handle. + +swagger:response deleteSignRuleInternalServerError +*/ +type DeleteSignRuleInternalServerError struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewDeleteSignRuleInternalServerError creates DeleteSignRuleInternalServerError with default headers values +func NewDeleteSignRuleInternalServerError() *DeleteSignRuleInternalServerError { + + return &DeleteSignRuleInternalServerError{} +} + +// WithPayload adds the payload to the delete sign rule internal server error response +func (o *DeleteSignRuleInternalServerError) WithPayload(payload *models.Error) *DeleteSignRuleInternalServerError { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the delete sign rule internal server error response +func (o *DeleteSignRuleInternalServerError) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DeleteSignRuleInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/api/server/restapi/operations/delete_sign_rule_urlbuilder.go b/api/server/restapi/operations/delete_sign_rule_urlbuilder.go new file mode 100644 index 000000000..2d9786ba9 --- /dev/null +++ b/api/server/restapi/operations/delete_sign_rule_urlbuilder.go @@ -0,0 +1,104 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// DeleteSignRuleURL generates an URL for the delete sign rule operation +type DeleteSignRuleURL struct { + Nickname string + RuleID string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *DeleteSignRuleURL) WithBasePath(bp string) *DeleteSignRuleURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *DeleteSignRuleURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *DeleteSignRuleURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/api/accounts/{nickname}/signrules/{ruleId}" + + nickname := o.Nickname + if nickname != "" { + _path = strings.Replace(_path, "{nickname}", nickname, -1) + } else { + return nil, errors.New("nickname is required on DeleteSignRuleURL") + } + + ruleID := o.RuleID + if ruleID != "" { + _path = strings.Replace(_path, "{ruleId}", ruleID, -1) + } else { + return nil, errors.New("ruleId is required on DeleteSignRuleURL") + } + + _basePath := o._basePath + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *DeleteSignRuleURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *DeleteSignRuleURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *DeleteSignRuleURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on DeleteSignRuleURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on DeleteSignRuleURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *DeleteSignRuleURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/api/server/restapi/operations/massa_wallet_api.go b/api/server/restapi/operations/massa_wallet_api.go index 22e4307bc..ea8df3d54 100644 --- a/api/server/restapi/operations/massa_wallet_api.go +++ b/api/server/restapi/operations/massa_wallet_api.go @@ -62,6 +62,9 @@ func NewMassaWalletAPI(spec *loads.Document) *MassaWalletAPI { AddAssetHandler: AddAssetHandlerFunc(func(params AddAssetParams) middleware.Responder { return middleware.NotImplemented("operation AddAsset has not yet been implemented") }), + AddSignRuleHandler: AddSignRuleHandlerFunc(func(params AddSignRuleParams) middleware.Responder { + return middleware.NotImplemented("operation AddSignRule has not yet been implemented") + }), BackupAccountHandler: BackupAccountHandlerFunc(func(params BackupAccountParams) middleware.Responder { return middleware.NotImplemented("operation BackupAccount has not yet been implemented") }), @@ -74,6 +77,9 @@ func NewMassaWalletAPI(spec *loads.Document) *MassaWalletAPI { DeleteAssetHandler: DeleteAssetHandlerFunc(func(params DeleteAssetParams) middleware.Responder { return middleware.NotImplemented("operation DeleteAsset has not yet been implemented") }), + DeleteSignRuleHandler: DeleteSignRuleHandlerFunc(func(params DeleteSignRuleParams) middleware.Responder { + return middleware.NotImplemented("operation DeleteSignRule has not yet been implemented") + }), ExportAccountFileHandler: ExportAccountFileHandlerFunc(func(params ExportAccountFileParams) middleware.Responder { return middleware.NotImplemented("operation ExportAccountFile has not yet been implemented") }), @@ -104,6 +110,9 @@ func NewMassaWalletAPI(spec *loads.Document) *MassaWalletAPI { UpdateAccountHandler: UpdateAccountHandlerFunc(func(params UpdateAccountParams) middleware.Responder { return middleware.NotImplemented("operation UpdateAccount has not yet been implemented") }), + UpdateSignRuleHandler: UpdateSignRuleHandlerFunc(func(params UpdateSignRuleParams) middleware.Responder { + return middleware.NotImplemented("operation UpdateSignRule has not yet been implemented") + }), DefaultPageHandler: DefaultPageHandlerFunc(func(params DefaultPageParams) middleware.Responder { return middleware.NotImplemented("operation DefaultPage has not yet been implemented") }), @@ -166,6 +175,8 @@ type MassaWalletAPI struct { AccountListHandler AccountListHandler // AddAssetHandler sets the operation handler for the add asset operation AddAssetHandler AddAssetHandler + // AddSignRuleHandler sets the operation handler for the add sign rule operation + AddSignRuleHandler AddSignRuleHandler // BackupAccountHandler sets the operation handler for the backup account operation BackupAccountHandler BackupAccountHandler // CreateAccountHandler sets the operation handler for the create account operation @@ -174,6 +185,8 @@ type MassaWalletAPI struct { DeleteAccountHandler DeleteAccountHandler // DeleteAssetHandler sets the operation handler for the delete asset operation DeleteAssetHandler DeleteAssetHandler + // DeleteSignRuleHandler sets the operation handler for the delete sign rule operation + DeleteSignRuleHandler DeleteSignRuleHandler // ExportAccountFileHandler sets the operation handler for the export account file operation ExportAccountFileHandler ExportAccountFileHandler // GetAccountHandler sets the operation handler for the get account operation @@ -194,6 +207,8 @@ type MassaWalletAPI struct { TransferCoinHandler TransferCoinHandler // UpdateAccountHandler sets the operation handler for the update account operation UpdateAccountHandler UpdateAccountHandler + // UpdateSignRuleHandler sets the operation handler for the update sign rule operation + UpdateSignRuleHandler UpdateSignRuleHandler // DefaultPageHandler sets the operation handler for the default page operation DefaultPageHandler DefaultPageHandler // WebAppHandler sets the operation handler for the web app operation @@ -296,6 +311,9 @@ func (o *MassaWalletAPI) Validate() error { if o.AddAssetHandler == nil { unregistered = append(unregistered, "AddAssetHandler") } + if o.AddSignRuleHandler == nil { + unregistered = append(unregistered, "AddSignRuleHandler") + } if o.BackupAccountHandler == nil { unregistered = append(unregistered, "BackupAccountHandler") } @@ -308,6 +326,9 @@ func (o *MassaWalletAPI) Validate() error { if o.DeleteAssetHandler == nil { unregistered = append(unregistered, "DeleteAssetHandler") } + if o.DeleteSignRuleHandler == nil { + unregistered = append(unregistered, "DeleteSignRuleHandler") + } if o.ExportAccountFileHandler == nil { unregistered = append(unregistered, "ExportAccountFileHandler") } @@ -338,6 +359,9 @@ func (o *MassaWalletAPI) Validate() error { if o.UpdateAccountHandler == nil { unregistered = append(unregistered, "UpdateAccountHandler") } + if o.UpdateSignRuleHandler == nil { + unregistered = append(unregistered, "UpdateSignRuleHandler") + } if o.DefaultPageHandler == nil { unregistered = append(unregistered, "DefaultPageHandler") } @@ -455,6 +479,10 @@ func (o *MassaWalletAPI) initHandlerCache() { if o.handlers["POST"] == nil { o.handlers["POST"] = make(map[string]http.Handler) } + o.handlers["POST"]["/api/accounts/{nickname}/signrules"] = NewAddSignRule(o.context, o.AddSignRuleHandler) + if o.handlers["POST"] == nil { + o.handlers["POST"] = make(map[string]http.Handler) + } o.handlers["POST"]["/api/accounts/{nickname}/backup"] = NewBackupAccount(o.context, o.BackupAccountHandler) if o.handlers["POST"] == nil { o.handlers["POST"] = make(map[string]http.Handler) @@ -468,6 +496,10 @@ func (o *MassaWalletAPI) initHandlerCache() { o.handlers["DELETE"] = make(map[string]http.Handler) } o.handlers["DELETE"]["/api/accounts/{nickname}/assets"] = NewDeleteAsset(o.context, o.DeleteAssetHandler) + if o.handlers["DELETE"] == nil { + o.handlers["DELETE"] = make(map[string]http.Handler) + } + o.handlers["DELETE"]["/api/accounts/{nickname}/signrules/{ruleId}"] = NewDeleteSignRule(o.context, o.DeleteSignRuleHandler) if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) } @@ -508,6 +540,10 @@ func (o *MassaWalletAPI) initHandlerCache() { o.handlers["PUT"] = make(map[string]http.Handler) } o.handlers["PUT"]["/api/accounts/{nickname}"] = NewUpdateAccount(o.context, o.UpdateAccountHandler) + if o.handlers["PUT"] == nil { + o.handlers["PUT"] = make(map[string]http.Handler) + } + o.handlers["PUT"]["/api/accounts/{nickname}/signrules/{ruleId}"] = NewUpdateSignRule(o.context, o.UpdateSignRuleHandler) if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) } diff --git a/api/server/restapi/operations/request.go b/api/server/restapi/operations/request.go new file mode 100644 index 000000000..83dc9e4ec --- /dev/null +++ b/api/server/restapi/operations/request.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// RequestHandlerFunc turns a function with the right signature into a request handler +type RequestHandlerFunc func(RequestParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn RequestHandlerFunc) Handle(params RequestParams) middleware.Responder { + return fn(params) +} + +// RequestHandler interface for that can handle valid request params +type RequestHandler interface { + Handle(RequestParams) middleware.Responder +} + +// NewRequest creates a new http.Handler for the request operation +func NewRequest(ctx *middleware.Context, handler RequestHandler) *Request { + return &Request{Context: ctx, Handler: handler} +} + +/* + Request swagger:route POST /api/accounts/{nickname}/request request + +Request user approval through UI. +*/ +type Request struct { + Context *middleware.Context + Handler RequestHandler +} + +func (o *Request) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewRequestParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/api/server/restapi/operations/request_parameters.go b/api/server/restapi/operations/request_parameters.go new file mode 100644 index 000000000..0005e60bc --- /dev/null +++ b/api/server/restapi/operations/request_parameters.go @@ -0,0 +1,108 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "io" + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" + "github.com/massalabs/station-massa-wallet/api/server/models" +) + +// NewRequestParams creates a new RequestParams object +// +// There are no default values defined in the spec. +func NewRequestParams() RequestParams { + + return RequestParams{} +} + +// RequestParams contains all the bound params for the request operation +// typically these are obtained from a http.Request +// +// swagger:parameters Request +type RequestParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: body + */ + Body *models.Request + /*Account's short name. + Required: true + In: path + */ + Nickname string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewRequestParams() beforehand. +func (o *RequestParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body models.Request + if err := route.Consumer.Consume(r.Body, &body); err != nil { + if err == io.EOF { + res = append(res, errors.Required("body", "body", "")) + } else { + res = append(res, errors.NewParseError("body", "body", "", err)) + } + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + ctx := validate.WithOperationRequest(r.Context()) + if err := body.ContextValidate(ctx, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.Body = &body + } + } + } else { + res = append(res, errors.Required("body", "body", "")) + } + + rNickname, rhkNickname, _ := route.Params.GetOK("nickname") + if err := o.bindNickname(rNickname, rhkNickname, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindNickname binds and validates parameter Nickname from path. +func (o *RequestParams) bindNickname(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.Nickname = raw + + return nil +} diff --git a/api/server/restapi/operations/request_responses.go b/api/server/restapi/operations/request_responses.go new file mode 100644 index 000000000..f5e0e5591 --- /dev/null +++ b/api/server/restapi/operations/request_responses.go @@ -0,0 +1,283 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + "github.com/massalabs/station-massa-wallet/api/server/models" +) + +// RequestOKCode is the HTTP code returned for type RequestOK +const RequestOKCode int = 200 + +/* +RequestOK Success. + +swagger:response requestOK +*/ +type RequestOK struct { + + /* + In: Body + */ + Payload *models.RequestResponse `json:"body,omitempty"` +} + +// NewRequestOK creates RequestOK with default headers values +func NewRequestOK() *RequestOK { + + return &RequestOK{} +} + +// WithPayload adds the payload to the request o k response +func (o *RequestOK) WithPayload(payload *models.RequestResponse) *RequestOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the request o k response +func (o *RequestOK) SetPayload(payload *models.RequestResponse) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *RequestOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// RequestBadRequestCode is the HTTP code returned for type RequestBadRequest +const RequestBadRequestCode int = 400 + +/* +RequestBadRequest Bad request. + +swagger:response requestBadRequest +*/ +type RequestBadRequest struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewRequestBadRequest creates RequestBadRequest with default headers values +func NewRequestBadRequest() *RequestBadRequest { + + return &RequestBadRequest{} +} + +// WithPayload adds the payload to the request bad request response +func (o *RequestBadRequest) WithPayload(payload *models.Error) *RequestBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the request bad request response +func (o *RequestBadRequest) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *RequestBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// RequestUnauthorizedCode is the HTTP code returned for type RequestUnauthorized +const RequestUnauthorizedCode int = 401 + +/* +RequestUnauthorized Unauthorized - The request requires user authentication. + +swagger:response requestUnauthorized +*/ +type RequestUnauthorized struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewRequestUnauthorized creates RequestUnauthorized with default headers values +func NewRequestUnauthorized() *RequestUnauthorized { + + return &RequestUnauthorized{} +} + +// WithPayload adds the payload to the request unauthorized response +func (o *RequestUnauthorized) WithPayload(payload *models.Error) *RequestUnauthorized { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the request unauthorized response +func (o *RequestUnauthorized) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *RequestUnauthorized) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(401) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// RequestNotFoundCode is the HTTP code returned for type RequestNotFound +const RequestNotFoundCode int = 404 + +/* +RequestNotFound Account Not found. + +swagger:response requestNotFound +*/ +type RequestNotFound struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewRequestNotFound creates RequestNotFound with default headers values +func NewRequestNotFound() *RequestNotFound { + + return &RequestNotFound{} +} + +// WithPayload adds the payload to the request not found response +func (o *RequestNotFound) WithPayload(payload *models.Error) *RequestNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the request not found response +func (o *RequestNotFound) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *RequestNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(404) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// RequestUnprocessableEntityCode is the HTTP code returned for type RequestUnprocessableEntity +const RequestUnprocessableEntityCode int = 422 + +/* +RequestUnprocessableEntity Unprocessable Entity - syntax is correct, but the server was unable to process the contained instructions. + +swagger:response requestUnprocessableEntity +*/ +type RequestUnprocessableEntity struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewRequestUnprocessableEntity creates RequestUnprocessableEntity with default headers values +func NewRequestUnprocessableEntity() *RequestUnprocessableEntity { + + return &RequestUnprocessableEntity{} +} + +// WithPayload adds the payload to the request unprocessable entity response +func (o *RequestUnprocessableEntity) WithPayload(payload *models.Error) *RequestUnprocessableEntity { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the request unprocessable entity response +func (o *RequestUnprocessableEntity) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *RequestUnprocessableEntity) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(422) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// RequestInternalServerErrorCode is the HTTP code returned for type RequestInternalServerError +const RequestInternalServerErrorCode int = 500 + +/* +RequestInternalServerError Internal Server Error - The server has encountered a situation it does not know how to handle. + +swagger:response requestInternalServerError +*/ +type RequestInternalServerError struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewRequestInternalServerError creates RequestInternalServerError with default headers values +func NewRequestInternalServerError() *RequestInternalServerError { + + return &RequestInternalServerError{} +} + +// WithPayload adds the payload to the request internal server error response +func (o *RequestInternalServerError) WithPayload(payload *models.Error) *RequestInternalServerError { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the request internal server error response +func (o *RequestInternalServerError) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *RequestInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/api/server/restapi/operations/request_urlbuilder.go b/api/server/restapi/operations/request_urlbuilder.go new file mode 100644 index 000000000..946a252f1 --- /dev/null +++ b/api/server/restapi/operations/request_urlbuilder.go @@ -0,0 +1,96 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// RequestURL generates an URL for the request operation +type RequestURL struct { + Nickname string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *RequestURL) WithBasePath(bp string) *RequestURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *RequestURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *RequestURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/api/accounts/{nickname}/request" + + nickname := o.Nickname + if nickname != "" { + _path = strings.Replace(_path, "{nickname}", nickname, -1) + } else { + return nil, errors.New("nickname is required on RequestURL") + } + + _basePath := o._basePath + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *RequestURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *RequestURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *RequestURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on RequestURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on RequestURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *RequestURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/api/server/restapi/operations/update_sign_rule.go b/api/server/restapi/operations/update_sign_rule.go new file mode 100644 index 000000000..ac298024c --- /dev/null +++ b/api/server/restapi/operations/update_sign_rule.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// UpdateSignRuleHandlerFunc turns a function with the right signature into a update sign rule handler +type UpdateSignRuleHandlerFunc func(UpdateSignRuleParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn UpdateSignRuleHandlerFunc) Handle(params UpdateSignRuleParams) middleware.Responder { + return fn(params) +} + +// UpdateSignRuleHandler interface for that can handle valid update sign rule params +type UpdateSignRuleHandler interface { + Handle(UpdateSignRuleParams) middleware.Responder +} + +// NewUpdateSignRule creates a new http.Handler for the update sign rule operation +func NewUpdateSignRule(ctx *middleware.Context, handler UpdateSignRuleHandler) *UpdateSignRule { + return &UpdateSignRule{Context: ctx, Handler: handler} +} + +/* + UpdateSignRule swagger:route PUT /api/accounts/{nickname}/signrules/{ruleId} updateSignRule + +Update an existing sign rule for the account associated with the provided nickname by its ID. +*/ +type UpdateSignRule struct { + Context *middleware.Context + Handler UpdateSignRuleHandler +} + +func (o *UpdateSignRule) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewUpdateSignRuleParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/api/server/restapi/operations/update_sign_rule_parameters.go b/api/server/restapi/operations/update_sign_rule_parameters.go new file mode 100644 index 000000000..47d8a4976 --- /dev/null +++ b/api/server/restapi/operations/update_sign_rule_parameters.go @@ -0,0 +1,132 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "io" + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" + "github.com/massalabs/station-massa-wallet/api/server/models" +) + +// NewUpdateSignRuleParams creates a new UpdateSignRuleParams object +// +// There are no default values defined in the spec. +func NewUpdateSignRuleParams() UpdateSignRuleParams { + + return UpdateSignRuleParams{} +} + +// UpdateSignRuleParams contains all the bound params for the update sign rule operation +// typically these are obtained from a http.Request +// +// swagger:parameters UpdateSignRule +type UpdateSignRuleParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: body + */ + Body *models.UpdateSignRule + /*Account's short name. + Required: true + In: path + */ + Nickname string + /*The ID of the sign rule + Required: true + In: path + */ + RuleID string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewUpdateSignRuleParams() beforehand. +func (o *UpdateSignRuleParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body models.UpdateSignRule + if err := route.Consumer.Consume(r.Body, &body); err != nil { + if err == io.EOF { + res = append(res, errors.Required("body", "body", "")) + } else { + res = append(res, errors.NewParseError("body", "body", "", err)) + } + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + ctx := validate.WithOperationRequest(r.Context()) + if err := body.ContextValidate(ctx, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.Body = &body + } + } + } else { + res = append(res, errors.Required("body", "body", "")) + } + + rNickname, rhkNickname, _ := route.Params.GetOK("nickname") + if err := o.bindNickname(rNickname, rhkNickname, route.Formats); err != nil { + res = append(res, err) + } + + rRuleID, rhkRuleID, _ := route.Params.GetOK("ruleId") + if err := o.bindRuleID(rRuleID, rhkRuleID, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindNickname binds and validates parameter Nickname from path. +func (o *UpdateSignRuleParams) bindNickname(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.Nickname = raw + + return nil +} + +// bindRuleID binds and validates parameter RuleID from path. +func (o *UpdateSignRuleParams) bindRuleID(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.RuleID = raw + + return nil +} diff --git a/api/server/restapi/operations/update_sign_rule_responses.go b/api/server/restapi/operations/update_sign_rule_responses.go new file mode 100644 index 000000000..230de184c --- /dev/null +++ b/api/server/restapi/operations/update_sign_rule_responses.go @@ -0,0 +1,283 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + "github.com/massalabs/station-massa-wallet/api/server/models" +) + +// UpdateSignRuleOKCode is the HTTP code returned for type UpdateSignRuleOK +const UpdateSignRuleOKCode int = 200 + +/* +UpdateSignRuleOK Sign rule updated successfully. + +swagger:response updateSignRuleOK +*/ +type UpdateSignRuleOK struct { + + /* + In: Body + */ + Payload *models.UpdateSignRuleResponse `json:"body,omitempty"` +} + +// NewUpdateSignRuleOK creates UpdateSignRuleOK with default headers values +func NewUpdateSignRuleOK() *UpdateSignRuleOK { + + return &UpdateSignRuleOK{} +} + +// WithPayload adds the payload to the update sign rule o k response +func (o *UpdateSignRuleOK) WithPayload(payload *models.UpdateSignRuleResponse) *UpdateSignRuleOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the update sign rule o k response +func (o *UpdateSignRuleOK) SetPayload(payload *models.UpdateSignRuleResponse) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *UpdateSignRuleOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// UpdateSignRuleBadRequestCode is the HTTP code returned for type UpdateSignRuleBadRequest +const UpdateSignRuleBadRequestCode int = 400 + +/* +UpdateSignRuleBadRequest Bad request. + +swagger:response updateSignRuleBadRequest +*/ +type UpdateSignRuleBadRequest struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewUpdateSignRuleBadRequest creates UpdateSignRuleBadRequest with default headers values +func NewUpdateSignRuleBadRequest() *UpdateSignRuleBadRequest { + + return &UpdateSignRuleBadRequest{} +} + +// WithPayload adds the payload to the update sign rule bad request response +func (o *UpdateSignRuleBadRequest) WithPayload(payload *models.Error) *UpdateSignRuleBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the update sign rule bad request response +func (o *UpdateSignRuleBadRequest) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *UpdateSignRuleBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// UpdateSignRuleUnauthorizedCode is the HTTP code returned for type UpdateSignRuleUnauthorized +const UpdateSignRuleUnauthorizedCode int = 401 + +/* +UpdateSignRuleUnauthorized Unauthorized - The request requires user authentication. + +swagger:response updateSignRuleUnauthorized +*/ +type UpdateSignRuleUnauthorized struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewUpdateSignRuleUnauthorized creates UpdateSignRuleUnauthorized with default headers values +func NewUpdateSignRuleUnauthorized() *UpdateSignRuleUnauthorized { + + return &UpdateSignRuleUnauthorized{} +} + +// WithPayload adds the payload to the update sign rule unauthorized response +func (o *UpdateSignRuleUnauthorized) WithPayload(payload *models.Error) *UpdateSignRuleUnauthorized { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the update sign rule unauthorized response +func (o *UpdateSignRuleUnauthorized) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *UpdateSignRuleUnauthorized) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(401) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// UpdateSignRuleNotFoundCode is the HTTP code returned for type UpdateSignRuleNotFound +const UpdateSignRuleNotFoundCode int = 404 + +/* +UpdateSignRuleNotFound Account or Rule Not found. + +swagger:response updateSignRuleNotFound +*/ +type UpdateSignRuleNotFound struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewUpdateSignRuleNotFound creates UpdateSignRuleNotFound with default headers values +func NewUpdateSignRuleNotFound() *UpdateSignRuleNotFound { + + return &UpdateSignRuleNotFound{} +} + +// WithPayload adds the payload to the update sign rule not found response +func (o *UpdateSignRuleNotFound) WithPayload(payload *models.Error) *UpdateSignRuleNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the update sign rule not found response +func (o *UpdateSignRuleNotFound) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *UpdateSignRuleNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(404) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// UpdateSignRuleUnprocessableEntityCode is the HTTP code returned for type UpdateSignRuleUnprocessableEntity +const UpdateSignRuleUnprocessableEntityCode int = 422 + +/* +UpdateSignRuleUnprocessableEntity Unprocessable Entity - syntax is correct, but the server was unable to process the contained instructions. + +swagger:response updateSignRuleUnprocessableEntity +*/ +type UpdateSignRuleUnprocessableEntity struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewUpdateSignRuleUnprocessableEntity creates UpdateSignRuleUnprocessableEntity with default headers values +func NewUpdateSignRuleUnprocessableEntity() *UpdateSignRuleUnprocessableEntity { + + return &UpdateSignRuleUnprocessableEntity{} +} + +// WithPayload adds the payload to the update sign rule unprocessable entity response +func (o *UpdateSignRuleUnprocessableEntity) WithPayload(payload *models.Error) *UpdateSignRuleUnprocessableEntity { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the update sign rule unprocessable entity response +func (o *UpdateSignRuleUnprocessableEntity) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *UpdateSignRuleUnprocessableEntity) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(422) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// UpdateSignRuleInternalServerErrorCode is the HTTP code returned for type UpdateSignRuleInternalServerError +const UpdateSignRuleInternalServerErrorCode int = 500 + +/* +UpdateSignRuleInternalServerError Internal Server Error - The server has encountered a situation it does not know how to handle. + +swagger:response updateSignRuleInternalServerError +*/ +type UpdateSignRuleInternalServerError struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewUpdateSignRuleInternalServerError creates UpdateSignRuleInternalServerError with default headers values +func NewUpdateSignRuleInternalServerError() *UpdateSignRuleInternalServerError { + + return &UpdateSignRuleInternalServerError{} +} + +// WithPayload adds the payload to the update sign rule internal server error response +func (o *UpdateSignRuleInternalServerError) WithPayload(payload *models.Error) *UpdateSignRuleInternalServerError { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the update sign rule internal server error response +func (o *UpdateSignRuleInternalServerError) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *UpdateSignRuleInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/api/server/restapi/operations/update_sign_rule_urlbuilder.go b/api/server/restapi/operations/update_sign_rule_urlbuilder.go new file mode 100644 index 000000000..5dabd7d4a --- /dev/null +++ b/api/server/restapi/operations/update_sign_rule_urlbuilder.go @@ -0,0 +1,104 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// UpdateSignRuleURL generates an URL for the update sign rule operation +type UpdateSignRuleURL struct { + Nickname string + RuleID string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *UpdateSignRuleURL) WithBasePath(bp string) *UpdateSignRuleURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *UpdateSignRuleURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *UpdateSignRuleURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/api/accounts/{nickname}/signrules/{ruleId}" + + nickname := o.Nickname + if nickname != "" { + _path = strings.Replace(_path, "{nickname}", nickname, -1) + } else { + return nil, errors.New("nickname is required on UpdateSignRuleURL") + } + + ruleID := o.RuleID + if ruleID != "" { + _path = strings.Replace(_path, "{ruleId}", ruleID, -1) + } else { + return nil, errors.New("ruleId is required on UpdateSignRuleURL") + } + + _basePath := o._basePath + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *UpdateSignRuleURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *UpdateSignRuleURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *UpdateSignRuleURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on UpdateSignRuleURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on UpdateSignRuleURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *UpdateSignRuleURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/api/walletApi-V0.yml b/api/walletApi-V0.yml index 1eca8438e..b9d075c38 100644 --- a/api/walletApi-V0.yml +++ b/api/walletApi-V0.yml @@ -196,7 +196,6 @@ paths: - in: body name: body required: true - x-nullable: false schema: $ref: "#/definitions/UpdateAccountRequest" produces: @@ -247,6 +246,114 @@ paths: description: Internal Server Error - The server has encountered a situation it does not know how to handle. schema: $ref: "#/definitions/Error" + + /api/accounts/{nickname}/signrules: + post: + operationId: AddSignRule + description: Create a new sign rule for the account associated with the provided nickname. + parameters: + - $ref: "#/parameters/nickname" + - in: body + name: body + required: true + schema: + $ref: "#/definitions/AddSignRule" + produces: + - application/json + responses: + 200: + description: signRule Id. + schema: + $ref: "#/definitions/AddSignRuleResponse" + 400: + description: Bad request. + schema: + $ref: "#/definitions/Error" + 401: + description: Unauthorized - The request requires user authentication. + schema: + $ref: "#/definitions/Error" + 404: + description: Account Not found. + schema: + $ref: "#/definitions/Error" + 422: + description: Unprocessable Entity - syntax is correct, but the server was unable to process the contained instructions. + schema: + $ref: "#/definitions/Error" + 500: + description: Internal Server Error - The server has encountered a situation it does not know how to handle. + schema: + $ref: "#/definitions/Error" + + /api/accounts/{nickname}/signrules/{ruleId}: + delete: + operationId: DeleteSignRule + description: Delete a sign rule for the account associated with the provided nickname by its ID. + parameters: + - $ref: "#/parameters/nickname" + - $ref: "#/parameters/ruleId" + produces: + - application/json + responses: + 200: + description: Sign rule deleted successfully. + 400: + description: Bad request. + schema: + $ref: "#/definitions/Error" + 401: + description: Unauthorized - The request requires user authentication. + schema: + $ref: "#/definitions/Error" + 404: + description: Account or Rule Not found. + schema: + $ref: "#/definitions/Error" + 500: + description: Internal Server Error - The server has encountered a situation it does not know how to handle. + schema: + $ref: "#/definitions/Error" + + put: + operationId: UpdateSignRule + description: Update an existing sign rule for the account associated with the provided nickname by its ID. + parameters: + - $ref: "#/parameters/nickname" + - $ref: "#/parameters/ruleId" + - in: body + name: body + required: true + schema: + $ref: "#/definitions/UpdateSignRule" + produces: + - application/json + responses: + 200: + description: Sign rule updated successfully. + schema: + $ref: "#/definitions/UpdateSignRuleResponse" + 400: + description: Bad request. + schema: + $ref: "#/definitions/Error" + 401: + description: Unauthorized - The request requires user authentication. + schema: + $ref: "#/definitions/Error" + 404: + description: Account or Rule Not found. + schema: + $ref: "#/definitions/Error" + 422: + description: Unprocessable Entity - syntax is correct, but the server was unable to process the contained instructions. + schema: + $ref: "#/definitions/Error" + 500: + description: Internal Server Error - The server has encountered a situation it does not know how to handle. + schema: + $ref: "#/definitions/Error" + /api/accounts/{nickname}/sign: post: operationId: Sign @@ -267,7 +374,7 @@ paths: description: Whether to allow user to edit the fee value in the sign prompt. x-nullable: true produces: - - application/json + - application/json responses: 200: description: Returns the signature, public key. @@ -501,6 +608,7 @@ paths: description: Internal Server Error - The server has encountered a situation it does not know how to handle. schema: $ref: "#/definitions/Error" + delete: operationId: DeleteAsset description: Delete token information from an account. @@ -550,6 +658,14 @@ parameters: default: true description: whether to return the data ciphered or not x-nullable: true + ruleId: + name: ruleId + in: path + required: true + type: string + description: The ID of the sign rule + x-nullable: false + definitions: Error: type: object @@ -584,6 +700,76 @@ definitions: type: integer description: The chain id of the network to which the operation will be sent. + AddSignRule: + type: object + required: + - ruleType + - contract + - enabled + properties: + description: + type: string + maxLength: 280 + description: Description text of what is being done (optional) + ruleType: + $ref: "#/definitions/RuleType" + name: + description: "The name of the rule." + type: "string" + contract: + description: "The contract to which the rule applies." + type: "string" + enabled: + description: "Whether the rule is enabled or not." + type: "boolean" + + RuleType: + description: "An enumeration of the different types of rules." + type: string + enum: + - disable_password_prompt + - auto_sign + x-nullable: false + + AddSignRuleResponse: + type: object + properties: + id: + type: string + x-nullable: false + readOnly: true + + UpdateSignRule: + type: object + required: + - ruleType + - contract + - enabled + properties: + description: + type: string + maxLength: 280 + description: Description text of what is being updated (optional) + ruleType: + $ref: "#/definitions/RuleType" + name: + description: "The name of the rule." + type: "string" + contract: + description: "The contract to which the rule applies. Use wildcard (*) to apply the rule for contracts." + type: "string" + enabled: + description: "Whether the rule is enabled or not." + type: "boolean" + + UpdateSignRuleResponse: + type: object + properties: + id: + type: string + x-nullable: false + readOnly: true + SignMessageRequest: type: object properties: @@ -669,6 +855,7 @@ definitions: format: byte x-nullable: false readOnly: true + Nickname: description: Account's short name. type: string @@ -771,14 +958,17 @@ definitions: properties: contract: type: string - passwordPrompt: - type: boolean - autoSign: + x-nullable: true + enabled: type: boolean + default: true + ruleType: + type: string + enum: + - password_prompt + - auto_sign required: - - contract - - passwordPrompt - - autoSign + - ruleType AccountConfig: type: object diff --git a/cmd/massa-wallet/massa-wallet.go b/cmd/massa-wallet/massa-wallet.go index a42c441cd..36698ecae 100644 --- a/cmd/massa-wallet/massa-wallet.go +++ b/cmd/massa-wallet/massa-wallet.go @@ -3,7 +3,6 @@ package app import ( "os" - "github.com/bluele/gcache" "github.com/massalabs/station-massa-hello-world/pkg/plugin" "github.com/massalabs/station-massa-wallet/api/server/restapi" "github.com/massalabs/station-massa-wallet/internal/handler" @@ -16,11 +15,6 @@ import ( ) func StartServer(app *walletApp.WalletApp) { - // Initialize cache - gc := gcache.New(20). - LRU(). - Build() - config.Load() massaClient := network.NewNodeFetcher() @@ -30,7 +24,7 @@ func StartServer(app *walletApp.WalletApp) { promptApp = prompt.NewEnvPrompter(app) } - AssetsStore, err := assets.NewAssetsStore("", massaClient) + _, err := assets.InitAssetsStore("", massaClient) if err != nil { logger.Fatalf("Failed to create AssetsStore: %v", err) } @@ -39,8 +33,6 @@ func StartServer(app *walletApp.WalletApp) { massaWalletAPI, err := handler.InitializeAPI( promptApp, massaClient, - AssetsStore, - gc, ) if err != nil { logger.Fatalf("Failed to initialize API: %v", err) diff --git a/go.mod b/go.mod index dc0786340..ac98b0410 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/rs/cors v1.8.3 github.com/shopspring/decimal v1.3.1 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.10.0 github.com/wailsapp/wails/v2 v2.9.1 golang.org/x/crypto v0.23.0 gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index db41a175d..15ea93f06 100644 --- a/go.sum +++ b/go.sum @@ -221,16 +221,16 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= diff --git a/internal/handler/api.go b/internal/handler/api.go index 08aba18d9..67f2cbf7e 100644 --- a/internal/handler/api.go +++ b/internal/handler/api.go @@ -1,19 +1,17 @@ package handler import ( - "github.com/bluele/gcache" "github.com/go-openapi/loads" "github.com/massalabs/station-massa-wallet/api/server/restapi" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" "github.com/massalabs/station-massa-wallet/internal/handler/html" walletHandler "github.com/massalabs/station-massa-wallet/internal/handler/wallet" - "github.com/massalabs/station-massa-wallet/pkg/assets" "github.com/massalabs/station-massa-wallet/pkg/network" "github.com/massalabs/station-massa-wallet/pkg/prompt" ) // InitializeAPI initializes the API handlers -func InitializeAPI(prompterApp prompt.WalletPrompterInterface, massaClient network.NodeFetcherInterface, AssetsStore *assets.AssetsStore, gc gcache.Cache) (*operations.MassaWalletAPI, error) { +func InitializeAPI(prompterApp prompt.WalletPrompterInterface, massaClient network.NodeFetcherInterface) (*operations.MassaWalletAPI, error) { // Load the Swagger specification swaggerSpec, err := loads.Analyzed(restapi.SwaggerJSON, "") if err != nil { @@ -27,7 +25,7 @@ func InitializeAPI(prompterApp prompt.WalletPrompterInterface, massaClient netwo html.AppendEndpoints(api) // Set wallet API endpoints - walletHandler.AppendEndpoints(api, prompterApp, massaClient, AssetsStore, gc) + walletHandler.AppendEndpoints(api, prompterApp, massaClient) return api, nil } diff --git a/internal/handler/wallet/add_assets_test.go b/internal/handler/wallet/add_assets_test.go index 6f7742fd4..5103353ae 100644 --- a/internal/handler/wallet/add_assets_test.go +++ b/internal/handler/wallet/add_assets_test.go @@ -3,7 +3,6 @@ package wallet import ( "encoding/json" "fmt" - "log" "net/http" "testing" @@ -11,16 +10,11 @@ import ( "github.com/massalabs/station-massa-wallet/api/server/models" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" "github.com/massalabs/station-massa-wallet/pkg/wallet/account" - "github.com/massalabs/station/pkg/logger" "github.com/stretchr/testify/assert" ) func TestAddAssetHandler(t *testing.T) { - if err := logger.InitializeGlobal("./unit-test.log"); err != nil { - log.Fatalf("while initializing global logger: %s", err.Error()) - } - - api, _, _, _, err := MockAPI() + api, _, err := MockAPI() assert.NoError(t, err) nickname := "GoodNickname" diff --git a/internal/handler/wallet/api.go b/internal/handler/wallet/api.go index 3fd8c1709..d73d7ff6e 100644 --- a/internal/handler/wallet/api.go +++ b/internal/handler/wallet/api.go @@ -5,15 +5,14 @@ import ( "fmt" "net/http" - "github.com/bluele/gcache" "github.com/go-openapi/runtime/middleware" "github.com/massalabs/station-massa-wallet/api/server/models" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" "github.com/massalabs/station-massa-wallet/pkg/assets" + "github.com/massalabs/station-massa-wallet/pkg/cache" "github.com/massalabs/station-massa-wallet/pkg/network" "github.com/massalabs/station-massa-wallet/pkg/openapi" "github.com/massalabs/station-massa-wallet/pkg/prompt" - "github.com/massalabs/station-massa-wallet/pkg/wallet" walletpkg "github.com/massalabs/station-massa-wallet/pkg/wallet" "github.com/massalabs/station-massa-wallet/pkg/wallet/account" "github.com/massalabs/station/pkg/logger" @@ -21,28 +20,33 @@ import ( // AppendEndpoints appends wallet endpoints to the API // Note: the password prompter is mandatory for sign endpoint -func AppendEndpoints(api *operations.MassaWalletAPI, prompterApp prompt.WalletPrompterInterface, massaClient network.NodeFetcherInterface, AssetsStore *assets.AssetsStore, gc gcache.Cache) { +func AppendEndpoints(api *operations.MassaWalletAPI, prompterApp prompt.WalletPrompterInterface, massaClient network.NodeFetcherInterface) { + gc := cache.Get() + assetStore := assets.Store api.CreateAccountHandler = NewCreateAccount(prompterApp, massaClient) api.DeleteAccountHandler = NewDelete(prompterApp, massaClient) api.ImportAccountHandler = NewImport(prompterApp, massaClient) api.AccountListHandler = NewGetAll(prompterApp.App().Wallet, massaClient) - api.SignHandler = NewSign(prompterApp, gc, AssetsStore) + api.SignHandler = NewSign(prompterApp, gc, assetStore) api.SignMessageHandler = NewSignMessage(prompterApp, gc) api.GetAccountHandler = NewGet(prompterApp, massaClient) api.ExportAccountFileHandler = NewWalletExportFile(prompterApp.App().Wallet) - api.TransferCoinHandler = NewTransferCoin(prompterApp, massaClient) + api.TransferCoinHandler = NewTransferCoin(prompterApp, massaClient, gc) api.TradeRollsHandler = NewTradeRolls(prompterApp, massaClient) api.BackupAccountHandler = NewBackupAccount(prompterApp) api.UpdateAccountHandler = NewUpdateAccount(prompterApp, massaClient) - api.AddAssetHandler = NewAddAsset(AssetsStore, massaClient) - api.GetAllAssetsHandler = NewGetAllAssets(prompterApp.App().Wallet, AssetsStore, massaClient) - api.DeleteAssetHandler = NewDeleteAsset(AssetsStore) + api.AddAssetHandler = NewAddAsset(assetStore, massaClient) + api.GetAllAssetsHandler = NewGetAllAssets(prompterApp.App().Wallet, assetStore, massaClient) + api.DeleteAssetHandler = NewDeleteAsset(assetStore) api.GetConfigHandler = NewGetConfig() + api.AddSignRuleHandler = NewAddSignRuleHandler(prompterApp, gc) + api.DeleteSignRuleHandler = NewDeleteSignRuleHandler(prompterApp, gc) + api.UpdateSignRuleHandler = NewUpdateSignRuleHandler(prompterApp, gc) } // loadAccount loads a wallet from the file system or returns an error. // Here it is acceptable to return a middleware.Responder to simplify the code. -func loadAccount(wallet *wallet.Wallet, nickname string) (*account.Account, middleware.Responder) { +func loadAccount(wallet *walletpkg.Wallet, nickname string) (*account.Account, middleware.Responder) { acc, err := wallet.GetAccount(nickname) if err == nil { return acc, nil diff --git a/internal/handler/wallet/api_test.go b/internal/handler/wallet/api_test.go index cb6d788ac..edf7bdb52 100644 --- a/internal/handler/wallet/api_test.go +++ b/internal/handler/wallet/api_test.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "strings" "testing" @@ -15,10 +16,11 @@ import ( "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" "github.com/massalabs/station-massa-wallet/pkg/assets" + "github.com/massalabs/station-massa-wallet/pkg/cache" "github.com/massalabs/station-massa-wallet/pkg/config" "github.com/massalabs/station-massa-wallet/pkg/network" - "github.com/massalabs/station-massa-wallet/pkg/prompt" "github.com/massalabs/station-massa-wallet/pkg/wallet" + "github.com/massalabs/station/pkg/logger" "github.com/stretchr/testify/assert" ) @@ -37,15 +39,21 @@ type PrivateKeyPrompt struct { Err error } +var ( + prompterAppMock *walletPrompterMock + testAssetStore *assets.AssetsStore + testCache gcache.Cache +) + // MockAPI mocks the wallet API. // All the wallet endpoints are mocked. You can use the Prompt channel to drive the password entry expected values. -func MockAPI() (*operations.MassaWalletAPI, prompt.WalletPrompterInterface, *assets.AssetsStore, chan walletapp.EventData, error) { +func MockAPI() (*operations.MassaWalletAPI, chan walletapp.EventData, error) { os.Setenv("STANDALONE", "1") // Load the Swagger specification swaggerSpec, err := loads.Analyzed(restapi.SwaggerJSON, "") if err != nil { - return nil, nil, nil, nil, err + return nil, nil, err } // Create a new MassaWalletAPI instance massaWalletAPI := operations.NewMassaWalletAPI(swaggerSpec) @@ -59,31 +67,38 @@ func MockAPI() (*operations.MassaWalletAPI, prompt.WalletPrompterInterface, *ass wallet, err := wallet.New(walletPath) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, err } - prompterApp := NewWalletPrompterMock(walletapp.NewWalletApp(wallet), resultChannel) + if err := logger.InitializeGlobal(filepath.Join(walletPath, "unit-test.log")); err != nil { + log.Fatalf("while initializing global logger: %s", err.Error()) + } + + prompterAppMock = NewWalletPrompterMock(walletapp.NewWalletApp(wallet), resultChannel) nodeFetcher := network.NewNodeFetcher() - AssetsStore, err := assets.NewAssetsStore(walletPath, nodeFetcher) + testAssetStore, err = assets.InitAssetsStore(walletPath, nodeFetcher) if err != nil { log.Fatalf("Failed to create AssetsStore: %v", err) } massaNodeMock := NewNodeFetcherMock() - // Load config file + // Load config file with config file path override + config.SetConfigFileDirOverride(walletPath) config.Load() + testCache = cache.Get() + // Set wallet API endpoints - AppendEndpoints(massaWalletAPI, prompterApp, massaNodeMock, AssetsStore, gcache.New(20).LRU().Build()) + AppendEndpoints(massaWalletAPI, prompterAppMock, massaNodeMock) // instantiates the server configure its API. server := restapi.NewServer(massaWalletAPI) server.ConfigureAPI() - return massaWalletAPI, prompterApp, AssetsStore, resultChannel, err + return massaWalletAPI, resultChannel, err } // processHTTPRequest simulates the processing of an HTTP request on the given API. diff --git a/internal/handler/wallet/backup_test.go b/internal/handler/wallet/backup_test.go index d407585ff..5e5641e8c 100644 --- a/internal/handler/wallet/backup_test.go +++ b/internal/handler/wallet/backup_test.go @@ -25,12 +25,12 @@ func backupWallet(t *testing.T, api *operations.MassaWalletAPI, nickname string) } func Test_walletBackupAccount_Handle(t *testing.T) { - api, prompterApp, _, resChan, err := MockAPI() + api, resChan, err := MockAPI() assert.NoError(t, err) nickname := "walletToBackup" password := "zePassword" - acc := createAccount(password, nickname, t, prompterApp) + acc := createAccount(password, nickname, t, prompterAppMock) t.Run("invalid nickname", func(t *testing.T) { resp := backupWallet(t, api, "toto") @@ -39,7 +39,7 @@ func Test_walletBackupAccount_Handle(t *testing.T) { t.Run("export canceled by user", func(t *testing.T) { go func() { - prompterApp.App().CtrlChan <- walletapp.Cancel + prompterAppMock.App().CtrlChan <- walletapp.Cancel }() resp := backupWallet(t, api, nickname) @@ -50,7 +50,7 @@ func Test_walletBackupAccount_Handle(t *testing.T) { testResult := make(chan walletapp.EventData) go func() { - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: string(prompt.YamlFileBackup), } @@ -68,11 +68,11 @@ func Test_walletBackupAccount_Handle(t *testing.T) { t.Run("chose private backup then cancel", func(t *testing.T) { go func() { // send backup method - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: string(prompt.PrivateKeyBackup), } - prompterApp.App().CtrlChan <- walletapp.Cancel + prompterAppMock.App().CtrlChan <- walletapp.Cancel }() resp := backupWallet(t, api, nickname) @@ -84,12 +84,12 @@ func Test_walletBackupAccount_Handle(t *testing.T) { go func() { // send backup method - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: string(prompt.PrivateKeyBackup), } // send password - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: "wrong password", } @@ -99,7 +99,7 @@ func Test_walletBackupAccount_Handle(t *testing.T) { checkResultChannel(t, result, false, utils.WrongPassword) // send password - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: password, } @@ -126,12 +126,12 @@ func Test_walletBackupAccount_Handle(t *testing.T) { t.Run("backup private key, wrong password and cancel", func(t *testing.T) { go func() { // send backup method - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: string(prompt.PrivateKeyBackup), } // send password - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: "wrong password", } @@ -140,7 +140,7 @@ func Test_walletBackupAccount_Handle(t *testing.T) { checkResultChannel(t, result, false, utils.WrongPassword) - prompterApp.App().CtrlChan <- walletapp.Cancel + prompterAppMock.App().CtrlChan <- walletapp.Cancel }() resp := backupWallet(t, api, nickname) @@ -152,12 +152,12 @@ func Test_walletBackupAccount_Handle(t *testing.T) { go func() { // send backup method - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: string(prompt.PrivateKeyBackup), } // send password - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: password, } diff --git a/internal/handler/wallet/cache.go b/internal/handler/wallet/cache.go new file mode 100644 index 000000000..a19c900d9 --- /dev/null +++ b/internal/handler/wallet/cache.go @@ -0,0 +1,155 @@ +package wallet + +import ( + "crypto/ed25519" + "fmt" + "os" + "time" + + "github.com/awnumar/memguard" + "github.com/bluele/gcache" + "github.com/massalabs/station-massa-wallet/pkg/utils" + "github.com/massalabs/station-massa-wallet/pkg/wallet/account" + "lukechampine.com/blake3" +) + +const ( + defaultExpirationTime = time.Hour * 24 * 30 + cacheKeyPrefix = "pkey" + notFoundMsg = "private key not found in cache" +) + +func CachePrivateKeyFromPassword(gc gcache.Cache, account *account.Account, password *memguard.LockedBuffer) error { + privateKey, err := account.PrivateKeyBytesInClear(password) + if err != nil { + return fmt.Errorf("error caching private key: %w", err) + } + + err = CachePrivateKey(gc, account, privateKey) + if err != nil { + return fmt.Errorf("error caching private key: %w", err) + } + + return nil +} + +func CachePrivateKey(gc gcache.Cache, account *account.Account, privateKey *memguard.LockedBuffer) error { + cacheKey, err := privateKeyCacheKey(account) + if err != nil { + return fmt.Errorf("%w: %w", utils.ErrPrivateKeyCache, err) + } + + key := KeyHash([]byte(cacheKey)) + + // Concatenate the key with itself to make it 64 bytes long + var extendedKey [64]byte + copy(extendedKey[:], append(key[:], key[:]...)) + + cipheredPrivateKey, err := Xor(privateKey, extendedKey) + if err != nil { + return fmt.Errorf("%w: %w", utils.ErrCache, err) + } + + cacheValue := make([]byte, ed25519.PrivateKeySize) + copy(cacheValue, cipheredPrivateKey.Bytes()) + cipheredPrivateKey.Destroy() + + err = gc.SetWithExpire(key, cacheValue, expirationDuration()) + if err != nil { + return fmt.Errorf("error set private key in cache: %w", err) + } + + return nil +} + +// privateKeyFromCache return the private key from the cache or an error. +func privateKeyFromCache( + gc gcache.Cache, + acc *account.Account, +) (*memguard.LockedBuffer, error) { + cacheKey, err := privateKeyCacheKey(acc) + if err != nil { + return nil, fmt.Errorf("%w: %w", utils.ErrPrivateKeyCache, err) + } + + keyHash := KeyHash([]byte(cacheKey)) + + value, err := gc.Get(keyHash) + if err != nil { + if err.Error() == gcache.KeyNotFoundError.Error() { + return nil, fmt.Errorf("%w: %w", utils.ErrPrivateKeyCache, err) + } + + return nil, fmt.Errorf("%w: %w", utils.ErrCache, err) + } + + if value == nil { + return nil, fmt.Errorf("%w: %s", utils.ErrCache, notFoundMsg) + } + + byteValue, ok := value.([]byte) + if !ok { + return nil, fmt.Errorf("%w: %s", utils.ErrCache, "value is not a byte array") + } + + cacheValue := make([]byte, ed25519.PrivateKeySize) + copy(cacheValue, byteValue) + + cipheredPrivateKey := memguard.NewBufferFromBytes(cacheValue) + + // Concatenate the key with itself to make it 64 bytes long + var extendedKey [64]byte + copy(extendedKey[:], append(keyHash[:], keyHash[:]...)) + + privateKey, err := Xor(cipheredPrivateKey, extendedKey) + if err != nil { + return nil, fmt.Errorf("%w: %w", utils.ErrCache, err) + } + + cipheredPrivateKey.Destroy() + + return privateKey, nil +} + +func Xor(pkey *memguard.LockedBuffer, cacheKeyHash [64]byte) (*memguard.LockedBuffer, error) { + a := pkey.Bytes() + + if len(a) != len(cacheKeyHash) { + return nil, fmt.Errorf("length of two arrays must be same, %d and %d", len(a), len(cacheKeyHash)) + } + result := make([]byte, len(a)) + + for i := 0; i < len(a); i++ { + result[i] = a[i] ^ cacheKeyHash[i] + } + + return memguard.NewBufferFromBytes(result), nil +} + +func privateKeyCacheKey(account *account.Account) ([]byte, error) { + address, err := account.Address.String() + if err != nil { + return []byte(""), fmt.Errorf("err: %w", err) + } + + return []byte(cacheKeyPrefix + address), nil +} + +func KeyHash(cacheKeyHash []byte) [32]byte { + return blake3.Sum256(cacheKeyHash) +} + +func expirationDuration() time.Duration { + fromEnv := os.Getenv("PKEY_CACHE_EXPIRATION_TIME") + + if fromEnv == "" { + return defaultExpirationTime + } + + duration, err := time.ParseDuration(fromEnv) + if err != nil { + return defaultExpirationTime + } + + return duration +} diff --git a/internal/handler/wallet/create_test.go b/internal/handler/wallet/create_test.go index 5399a1402..fa9a6a7b2 100644 --- a/internal/handler/wallet/create_test.go +++ b/internal/handler/wallet/create_test.go @@ -14,7 +14,7 @@ import ( ) func Test_walletCreate_Handle(t *testing.T) { - api, prompterApp, _, resChan, err := MockAPI() + api, resChan, err := MockAPI() if err != nil { panic(err) } @@ -41,9 +41,9 @@ func Test_walletCreate_Handle(t *testing.T) { go func(res chan walletapp.EventData) { if test.password == "cancel" { // Send cancel to prompter app to unlock the handler - prompterApp.App().CtrlChan <- walletapp.Cancel + prompterAppMock.App().CtrlChan <- walletapp.Cancel } else { - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: test.password, } @@ -60,7 +60,7 @@ func Test_walletCreate_Handle(t *testing.T) { checkResultChannel(t, result, true, "") - assertWallet(t, prompterApp.App().Wallet, nickname) + assertWallet(t, prompterAppMock.App().Wallet, nickname) } } } diff --git a/internal/handler/wallet/delete_asset_test.go b/internal/handler/wallet/delete_asset_test.go index ecef7d688..98a5d0cae 100644 --- a/internal/handler/wallet/delete_asset_test.go +++ b/internal/handler/wallet/delete_asset_test.go @@ -10,12 +10,12 @@ import ( ) func TestAddGetDeleteAsset(t *testing.T) { - api, prompterApp, _, _, err := MockAPI() + api, _, err := MockAPI() assert.NoError(t, err) nickname := "GoodNickname" password := "zePassword" - createAccount(password, nickname, t, prompterApp) + createAccount(password, nickname, t, prompterAppMock) // Create the test wallet first diff --git a/internal/handler/wallet/delete_test.go b/internal/handler/wallet/delete_test.go index dc90e2ad0..b0fe362fd 100644 --- a/internal/handler/wallet/delete_test.go +++ b/internal/handler/wallet/delete_test.go @@ -23,14 +23,14 @@ func deleteWallet(t *testing.T, api *operations.MassaWalletAPI, nickname string) } func Test_walletDelete_Handle(t *testing.T) { - api, prompterApp, _, resChan, err := MockAPI() + api, resChan, err := MockAPI() assert.NoError(t, err) testResult := make(chan walletapp.EventData) nickname := "walletToDelete" password := "zePassword" - createAccount(password, nickname, t, prompterApp) + createAccount(password, nickname, t, prompterAppMock) t.Run("invalid nickname", func(t *testing.T) { resp := deleteWallet(t, api, "toto") @@ -40,7 +40,7 @@ func Test_walletDelete_Handle(t *testing.T) { t.Run("invalid password", func(t *testing.T) { // Send password to prompter app and wait for result go func() { - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: "invalid password", } @@ -50,7 +50,7 @@ func Test_walletDelete_Handle(t *testing.T) { checkResultChannel(t, failRes, false, utils.WrongPassword) // Send cancel to prompter app to unlock the handler - prompterApp.App().CtrlChan <- walletapp.Cancel + prompterAppMock.App().CtrlChan <- walletapp.Cancel }() resp := deleteWallet(t, api, nickname) @@ -60,7 +60,7 @@ func Test_walletDelete_Handle(t *testing.T) { t.Run("canceled by user", func(t *testing.T) { go func() { // Send wrong password to prompter app and wait for result - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: "this is not the password", } @@ -70,21 +70,21 @@ func Test_walletDelete_Handle(t *testing.T) { checkResultChannel(t, failRes, false, utils.WrongPassword) // Send cancel to prompter app to unlock the handler - prompterApp.App().CtrlChan <- walletapp.Cancel + prompterAppMock.App().CtrlChan <- walletapp.Cancel }() resp := deleteWallet(t, api, nickname) verifyStatusCode(t, resp, http.StatusUnauthorized) - _, err = prompterApp.App().Wallet.GetAccount(nickname) + _, err = prompterAppMock.App().Wallet.GetAccount(nickname) assert.NoError(t, err) }) t.Run("delete success", func(t *testing.T) { // Send password to prompter app and wait for result go func() { - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: password, } @@ -100,7 +100,7 @@ func Test_walletDelete_Handle(t *testing.T) { checkResultChannel(t, result, true, "") - _, err = prompterApp.App().Wallet.GetAccount(nickname) + _, err = prompterAppMock.App().Wallet.GetAccount(nickname) assert.Error(t, err, "Wallet should have been deleted") }) diff --git a/internal/handler/wallet/errors.go b/internal/handler/wallet/errors.go index 36e710de0..6f170fa0a 100644 --- a/internal/handler/wallet/errors.go +++ b/internal/handler/wallet/errors.go @@ -32,4 +32,9 @@ const ( errorAssetNotFound errorTradeRoll internalError + errorAddSignRule + errorDeleteSignRule + errorUpdateSignRule + errorCachePrivateKey + errorGetAccountConfig ) diff --git a/internal/handler/wallet/export_test.go b/internal/handler/wallet/export_test.go index b803e50c1..2d731397c 100644 --- a/internal/handler/wallet/export_test.go +++ b/internal/handler/wallet/export_test.go @@ -10,7 +10,8 @@ import ( ) func Test_exportFileWallet_handler(t *testing.T) { - api, prompterApp, _, _, err := MockAPI() + api, _, err := MockAPI() + assert.NoError(t, err) handler, exist := api.HandlerFor("get", "/api/accounts/{nickname}/exportFile") @@ -27,7 +28,7 @@ func Test_exportFileWallet_handler(t *testing.T) { t.Run("Export file of created wallet", func(t *testing.T) { nickname := "trololol" password := "zePassword" - createAccount(password, nickname, t, prompterApp) + createAccount(password, nickname, t, prompterAppMock) resp, err := handleHTTPRequest(handler, "GET", fmt.Sprintf("/api/accounts/%s/exportFile", nickname), "") assert.NoError(t, err) diff --git a/internal/handler/wallet/get_all_assets_test.go b/internal/handler/wallet/get_all_assets_test.go index 88c31898b..c8df4c46d 100644 --- a/internal/handler/wallet/get_all_assets_test.go +++ b/internal/handler/wallet/get_all_assets_test.go @@ -15,12 +15,12 @@ import ( ) func TestGetAllAssetsHandler(t *testing.T) { - api, prompterApp, _, _, err := MockAPI() + api, _, err := MockAPI() assert.NoError(t, err) nickname := "GoodNickname" password := "zePassword" - createAccount(password, nickname, t, prompterApp) + createAccount(password, nickname, t, prompterAppMock) // Get the assetsWithBalance assetsWithBalance := getAssets(t, api, nickname) @@ -66,7 +66,7 @@ func getExpectedAssetsCount(t *testing.T) int { tempDir, err := os.MkdirTemp(os.TempDir(), "*-wallet-dir") assert.NoError(t, err) nodeFetcher := network.NewNodeFetcher() - store, err := assets.NewAssetsStore(tempDir, nodeFetcher) + store, err := assets.InitAssetsStore(tempDir, nodeFetcher) assert.NoError(t, err) defaultAssets, err := store.Default() assert.NoError(t, err) diff --git a/internal/handler/wallet/get_config.go b/internal/handler/wallet/get_config.go index 952b68321..5f4769cd5 100644 --- a/internal/handler/wallet/get_config.go +++ b/internal/handler/wallet/get_config.go @@ -35,9 +35,9 @@ func newConfigModel(cfg *config.Config) (*models.Config, error) { modelSignRules := make([]*models.SignRule, len(accountConfig.SignRules)) for i, rule := range accountConfig.SignRules { modelSignRules[i] = &models.SignRule{ - Contract: &rule.Contract, - PasswordPrompt: &rule.PasswordPrompt, - AutoSign: &rule.AutoSign, + Contract: &rule.Contract, + Enabled: &rule.Enabled, + RuleType: (*string)(&rule.RuleType), } } modelAccounts[nickname] = models.AccountConfig{ diff --git a/internal/handler/wallet/get_test.go b/internal/handler/wallet/get_test.go index 32ec9f49f..1bb2100b7 100644 --- a/internal/handler/wallet/get_test.go +++ b/internal/handler/wallet/get_test.go @@ -14,10 +14,10 @@ import ( ) func Test_getWallets_handler(t *testing.T) { - api, prompterApp, _, _, err := MockAPI() + api, _, err := MockAPI() assert.NoError(t, err) - wallet.ClearAccounts(t, prompterApp.App().Wallet.WalletPath) + wallet.ClearAccounts(t, prompterAppMock.App().Wallet.WalletPath) // test empty configuration first. t.Run("Get empty list", func(t *testing.T) { @@ -38,7 +38,7 @@ func Test_getWallets_handler(t *testing.T) { nicknames := []string{"account1", "account2", "account3"} for _, nickname := range nicknames { - createAccount(password, nickname, t, prompterApp) + createAccount(password, nickname, t, prompterAppMock) } t.Run("Get multiple accounts", func(t *testing.T) { @@ -58,17 +58,17 @@ func Test_getWallets_handler(t *testing.T) { assertAccountsBody(t, resp, true) }) - wallet.ClearAccounts(t, prompterApp.App().Wallet.WalletPath) + wallet.ClearAccounts(t, prompterAppMock.App().Wallet.WalletPath) } func Test_getWallet_handler(t *testing.T) { nickname := "trololol" password := "zePassword" - api, prompterApp, _, resChan, err := MockAPI() + api, resChan, err := MockAPI() assert.NoError(t, err) - createAccount(password, nickname, t, prompterApp) + createAccount(password, nickname, t, prompterAppMock) handler, exist := api.HandlerFor("get", "/api/accounts/{nickname}") assert.True(t, exist, "Endpoint doesn't exist") @@ -88,7 +88,7 @@ func Test_getWallet_handler(t *testing.T) { assertAccountBody(t, resp, nickname, true) - wallet.ClearAccounts(t, prompterApp.App().Wallet.WalletPath) + wallet.ClearAccounts(t, prompterAppMock.App().Wallet.WalletPath) }) // test with un-ciphered data. @@ -96,7 +96,7 @@ func Test_getWallet_handler(t *testing.T) { testResult := make(chan walletapp.EventData) // Send password to prompter app and wait for result go func(res chan walletapp.EventData) { - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: password, } diff --git a/internal/handler/wallet/import_test.go b/internal/handler/wallet/import_test.go index 7f7ee3969..bcde3f7e2 100644 --- a/internal/handler/wallet/import_test.go +++ b/internal/handler/wallet/import_test.go @@ -27,7 +27,7 @@ func importWallet(t *testing.T, api *operations.MassaWalletAPI) *httptest.Respon } func Test_walletImport_Handle(t *testing.T) { - api, prompterApp, _, resChan, err := MockAPI() + api, resChan, err := MockAPI() assert.NoError(t, err) t.Run("import wallet file", func(t *testing.T) { @@ -58,7 +58,7 @@ PublicKey: [0, 164, 243, 44, 155, 204, 6, 20, 131, 218, 97, 32, 58, 224, 189, 41 // Send filepath to prompter app and wait for result go func(res chan walletapp.EventData) { - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: filePath, } @@ -74,7 +74,7 @@ PublicKey: [0, 164, 243, 44, 155, 204, 6, 20, 131, 218, 97, 32, 58, 224, 189, 41 checkResultChannel(t, result, true, "") - assertWallet(t, prompterApp.App().Wallet, nickname) + assertWallet(t, prompterAppMock.App().Wallet, nickname) os.Remove(filePath) }) @@ -95,7 +95,7 @@ PublicKey: [0, 164, 243, 44, 155, 204, 6, 20, 131, 218, 97, 32, 58, 224, 189, 41 // Send filepath to prompter app and wait for result go func(res chan walletapp.EventData) { // Send invalid file to prompter app and wait for result - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: filePath, } @@ -140,7 +140,7 @@ PublicKey: [0, 164, 243, 44, 155, 204, 6, 20, 131, 218, 97, 32, 58, 224, 189, 41 // Send filepath to prompter app and wait for result go func(res chan walletapp.EventData) { // Send invalid filename to prompter app and wait for result - prompterApp.App().PromptInput <- &walletapp.StringPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ BaseMessage: walletapp.BaseMessage{}, Message: filePath, } @@ -201,7 +201,7 @@ PublicKey: [0, 164, 243, 44, 155, 204, 6, 20, 131, 218, 97, 32, 58, 224, 189, 41 testResult := make(chan walletapp.EventData) go func(res chan walletapp.EventData) { - prompterApp.App().PromptInput <- &walletapp.ImportPKeyPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.ImportPKeyPromptInput{ BaseMessage: walletapp.BaseMessage{}, PrivateKey: tt.privateKey, Nickname: tt.nickname, @@ -217,7 +217,7 @@ PublicKey: [0, 164, 243, 44, 155, 204, 6, 20, 131, 218, 97, 32, 58, 224, 189, 41 checkResultChannel(t, result, tt.wantResult.Success, tt.wantResult.CodeMessage) if tt.wantResult.Success { - assertWallet(t, prompterApp.App().Wallet, tt.nickname) + assertWallet(t, prompterAppMock.App().Wallet, tt.nickname) } }) } diff --git a/internal/handler/wallet/prompt_utils.go b/internal/handler/wallet/prompt_utils.go new file mode 100644 index 000000000..dbbeb3086 --- /dev/null +++ b/internal/handler/wallet/prompt_utils.go @@ -0,0 +1,45 @@ +package wallet + +import ( + "fmt" + + "github.com/awnumar/memguard" + walletapp "github.com/massalabs/station-massa-wallet/pkg/app" + "github.com/massalabs/station-massa-wallet/pkg/prompt" + "github.com/massalabs/station-massa-wallet/pkg/utils" + "github.com/massalabs/station-massa-wallet/pkg/wallet/account" +) + +func PromptPassword(prompterApp prompt.WalletPrompterInterface, acc *account.Account, promptRequest *prompt.PromptRequest) (*memguard.LockedBuffer, error) { + promptOutput, err := prompt.WakeUpPrompt(prompterApp, *promptRequest, acc) + if err != nil { + return nil, fmt.Errorf("prompting password: %w", err) + } + + output, ok := promptOutput.(*memguard.LockedBuffer) + if !ok { + return nil, fmt.Errorf("prompting password: %s", utils.ErrInvalidInputType.Error()) + } + + prompterApp.EmitEvent(walletapp.PromptResultEvent, + walletapp.EventData{Success: true}) + + return output, nil +} + +func PromptForOperation(prompterApp prompt.WalletPrompterInterface, acc *account.Account, promptRequest *prompt.PromptRequest) (*walletapp.SignPromptOutput, error) { + promptOutput, err := prompt.WakeUpPrompt(prompterApp, *promptRequest, acc) + if err != nil { + return nil, fmt.Errorf("prompting password: %w", err) + } + + output, ok := promptOutput.(*walletapp.SignPromptOutput) + if !ok { + return nil, fmt.Errorf("prompting password: %s", utils.ErrInvalidInputType.Error()) + } + + prompterApp.EmitEvent(walletapp.PromptResultEvent, + walletapp.EventData{Success: true}) + + return output, nil +} diff --git a/internal/handler/wallet/rolls.go b/internal/handler/wallet/rolls.go index 7e183dc17..e32d40ffc 100644 --- a/internal/handler/wallet/rolls.go +++ b/internal/handler/wallet/rolls.go @@ -66,7 +66,7 @@ func (t *tradeRolls) Handle(params operations.TradeRollsParams) middleware.Respo promptRequest := prompt.PromptRequest{ Action: walletapp.Sign, - Data: PromptRequestSignData{ + Data: prompt.PromptRequestSignData{ Fees: strconv.FormatUint(fee, 10), MinFees: minimalFees, WalletAddress: string(addressBytes), diff --git a/internal/handler/wallet/rolls_test.go b/internal/handler/wallet/rolls_test.go index 2d598ba81..37d57c3c0 100644 --- a/internal/handler/wallet/rolls_test.go +++ b/internal/handler/wallet/rolls_test.go @@ -10,7 +10,7 @@ import ( ) func Test_traderolls_handler(t *testing.T) { - api, prompterApp, _, resChan, err := MockAPI() + api, resChan, err := MockAPI() assert.NoError(t, err) handler, exist := api.HandlerFor("post", "/api/accounts/{nickname}/rolls") @@ -18,7 +18,7 @@ func Test_traderolls_handler(t *testing.T) { nickname := "wallet1" password := "password" - createAccount(password, nickname, t, prompterApp) + createAccount(password, nickname, t, prompterAppMock) t.Run("Trade rolls with unprocessable entity", func(t *testing.T) { resp, err := handleHTTPRequest(handler, "POST", fmt.Sprintf("/api/accounts/%s/rolls", nickname), "") @@ -84,7 +84,7 @@ func Test_traderolls_handler(t *testing.T) { // Send password to prompter app and wait for result go func(res chan walletapp.EventData) { - prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ BaseMessage: walletapp.BaseMessage{}, Password: password, Fees: "1000", @@ -111,7 +111,7 @@ func Test_traderolls_handler(t *testing.T) { // Send password to prompter app and wait for result go func(res chan walletapp.EventData) { - prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ BaseMessage: walletapp.BaseMessage{}, Password: password, Fees: "1000", diff --git a/internal/handler/wallet/sign.go b/internal/handler/wallet/sign.go index f7f94e61c..88195a2a3 100644 --- a/internal/handler/wallet/sign.go +++ b/internal/handler/wallet/sign.go @@ -1,13 +1,10 @@ package wallet import ( - "crypto/ed25519" "encoding/binary" "fmt" "net/http" - "os" "strconv" - "time" "github.com/awnumar/memguard" "github.com/bluele/gcache" @@ -16,10 +13,13 @@ import ( "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" "github.com/massalabs/station-massa-wallet/pkg/assets" + "github.com/massalabs/station-massa-wallet/pkg/cache" + "github.com/massalabs/station-massa-wallet/pkg/config" "github.com/massalabs/station-massa-wallet/pkg/network" "github.com/massalabs/station-massa-wallet/pkg/prompt" "github.com/massalabs/station-massa-wallet/pkg/utils" "github.com/massalabs/station-massa-wallet/pkg/wallet/account" + "github.com/massalabs/station/pkg/logger" "github.com/massalabs/station/pkg/node/sendoperation" "github.com/massalabs/station/pkg/node/sendoperation/buyrolls" "github.com/massalabs/station/pkg/node/sendoperation/callsc" @@ -28,39 +28,12 @@ import ( "github.com/massalabs/station/pkg/node/sendoperation/transaction" onchain "github.com/massalabs/station/pkg/onchain" "github.com/pkg/errors" - "lukechampine.com/blake3" ) const ( - defaultExpirationTime = time.Hour * 24 * 30 - RollPrice = 100 - cacheKeyPrefix = "pkey" + RollPrice = 100 ) -type PromptRequestSignData struct { - Description string - Fees string - MinFees string - OperationType int - Coins string - Address string - Function string - MaxCoins string // for ExecuteSC - WalletAddress string - Nickname string - RollCount uint64 - RecipientAddress string - RecipientNickname string - Amount string - PlainText string - AllowFeeEdition bool - ChainID int64 - Assets []models.AssetInfo - Parameters []byte - DeployedByteCodeSize uint // for executeSC of type deploySC - DeployedCoins uint64 // for executeSC of type DeploySC; the number of coins sent to the deployed contract -} - func NewSign(prompterApp prompt.WalletPrompterInterface, gc gcache.Cache, AssetsStore *assets.AssetsStore) operations.SignHandler { return &walletSign{gc: gc, prompterApp: prompterApp, AssetsStore: AssetsStore} } @@ -77,30 +50,43 @@ func (w *walletSign) Handle(params operations.SignParams) middleware.Responder { return errResp } - promptRequest, fees, err := w.getPromptRequest(params, acc, params.Body.Description, *params.Body.ChainID) + promptRequest, fees, err := w.getPromptRequest(params, acc) if err != nil { return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorSignDecodeMessage, http.StatusBadRequest) } - var privateKey *memguard.LockedBuffer + cfg := config.Get() - // accountCfg, err := config.GetAccountConfig(params.Nickname) + var contract *string - if err != nil { - // todo - // todo: handle cache expired error - pk, err := w.privateKeyFromCache(acc) - if err != nil { - if errors.Is(err, errors.New("errtodo")) { - return newErrorResponse(err.Error(), "errtodo", http.StatusNotFound) - } + promptData, ok := promptRequest.Data.(prompt.PromptRequestSignData) + if ok { + contract = &promptData.Address + } + + enabledRule := cfg.GetEnabledRuleForContract(acc.Nickname, contract) + + var privateKey *memguard.LockedBuffer - return newErrorResponse(err.Error(), errorSign, http.StatusInternalServerError) + if enabledRule != nil { + // at this point, we have a rule enabled for the contract, if private key is cached, we don't need to prompt for password + privateKey, err = cache.PrivateKeyFromCache(w.gc, acc) + if err != nil { + logger.Warn("error retriving private key from cache: ", err) + } else if privateKey != nil { + // If privatekey is cached, we don't need to prompt for password + promptRequest.PasswordRequired = false } - privateKey = pk - } else { - output, err := w.PromptPassword(acc, promptRequest) + } + + isDisablePasswordRuleEnabled := enabledRule != nil && *enabledRule == config.RuleTypeDisablePasswordPrompt + + if promptRequest.PasswordRequired || isDisablePasswordRuleEnabled { + promptData.EnabledSignRule = enabledRule + promptRequest.Data = promptData + + output, err := PromptForOperation(w.prompterApp, acc, promptRequest) if err != nil { msg := fmt.Sprintf("Unable to unprotect wallet: %s", err.Error()) if errors.Is(err, utils.ErrWrongPassword) || errors.Is(err, utils.ErrActionCanceled) { @@ -112,17 +98,19 @@ func (w *walletSign) Handle(params operations.SignParams) middleware.Responder { fees = output.Fees - pk, err := acc.PrivateKeyBytesInClear(output.Password) - if err != nil { - return newErrorResponse(err.Error(), errorWrongPassword, http.StatusInternalServerError) + if privateKey == nil { + if output.Password != nil { + privateKey, err = acc.PrivateKeyBytesInClear(output.Password) + if err != nil { + return newErrorResponse(err.Error(), errorWrongPassword, http.StatusInternalServerError) + } + } } - privateKey = pk - - if false { // todo - err := w.CacheAccount(acc, privateKey) + if cfg.HasEnabledRule(acc.Nickname) { + err = cache.CachePrivateKey(w.gc, acc, privateKey) if err != nil { - return newErrorResponse(err.Error(), "errtodo", http.StatusInternalServerError) + return newErrorResponse(err.Error(), errorCachePrivateKey, http.StatusInternalServerError) } } } @@ -141,45 +129,6 @@ func (w *walletSign) Handle(params operations.SignParams) middleware.Responder { return w.Success(acc, signature, operation) } -func (w *walletSign) PromptPassword(acc *account.Account, promptRequest *prompt.PromptRequest) (*walletapp.SignPromptOutput, error) { - promptOutput, err := prompt.WakeUpPrompt(w.prompterApp, *promptRequest, acc) - if err != nil { - return nil, fmt.Errorf("prompting password: %w", err) - } - - output, ok := promptOutput.(*walletapp.SignPromptOutput) - if !ok { - return nil, fmt.Errorf("prompting password for sign: %s", utils.ErrInvalidInputType.Error()) - } - - w.prompterApp.EmitEvent(walletapp.PromptResultEvent, - walletapp.EventData{Success: true}) - - return output, nil -} - -func (w *walletSign) CacheAccount(acc *account.Account, privateKey *memguard.LockedBuffer) error { - cacheKey := cacheKeyPrefix + acc.Nickname - - key := KeyHash([]byte(cacheKey)) - - cipheredPrivateKey, err := Xor(privateKey, key) - if err != nil { - return fmt.Errorf("Error cannot XOR private key: %w", err) - } - - cacheValue := make([]byte, ed25519.PrivateKeySize) - copy(cacheValue, cipheredPrivateKey.Bytes()) - cipheredPrivateKey.Destroy() - - err = w.gc.SetWithExpire(key, cacheValue, expirationDuration()) - if err != nil { - return fmt.Errorf("Error set private key in cache: %w", err) - } - - return nil -} - func (w *walletSign) Success(acc *account.Account, signature []byte, operation []byte) middleware.Responder { publicKeyBytes, err := acc.PublicKey.MarshalText() if err != nil { @@ -214,7 +163,7 @@ func prepareOperation(acc *account.Account, fees uint64, operationB64 string, ch publicKey, err := acc.PublicKey.MarshalBinary() if err != nil { - return nil, nil, fmt.Errorf("Unable to marshal public key: %w", err) + return nil, nil, fmt.Errorf("unable to marshal public key: %w", err) } msgToSign = utils.PrepareSignData(uint64(chainID), append(publicKey, msgToSign...)) @@ -222,7 +171,7 @@ func prepareOperation(acc *account.Account, fees uint64, operationB64 string, ch return operation, msgToSign, nil } -func (w *walletSign) getPromptRequest(params operations.SignParams, acc *account.Account, description string, chainID int64) (*prompt.PromptRequest, uint64, error) { +func (w *walletSign) getPromptRequest(params operations.SignParams, acc *account.Account) (*prompt.PromptRequest, uint64, error) { msgToSign := params.Body.Operation.String() decodedMsg, fees, _, err := sendoperation.DecodeMessage64(msgToSign) @@ -235,7 +184,7 @@ func (w *walletSign) getPromptRequest(params operations.SignParams, acc *account return nil, 0, fmt.Errorf("failed to decode operation ID: %w", err) } - var data PromptRequestSignData + var data prompt.PromptRequestSignData switch opType { case transaction.OpType: @@ -258,9 +207,9 @@ func (w *walletSign) getPromptRequest(params operations.SignParams, acc *account return nil, 0, fmt.Errorf("failed to decode message of operation type: %d: %w", opType, err) } - addressBytes, err := acc.Address.MarshalText() + address, err := acc.Address.String() if err != nil { - return nil, 0, fmt.Errorf("failed to marshal address: %w", err) + return nil, 0, fmt.Errorf("err: %w", err) } _, minimalFees, err := network.GetNodeInfo() @@ -268,19 +217,20 @@ func (w *walletSign) getPromptRequest(params operations.SignParams, acc *account minimalFees = "0" } - data.Description = description + data.Description = params.Body.Description data.Fees = strconv.FormatUint(fees, 10) data.MinFees = minimalFees - data.WalletAddress = string(addressBytes) + data.WalletAddress = address data.Nickname = acc.Nickname data.OperationType = int(opType) data.AllowFeeEdition = *params.AllowFeeEdition - data.ChainID = chainID - data.Assets = convertAssetsToModel(w.AssetsStore.All(acc.Nickname, int(chainID))) + data.ChainID = *params.Body.ChainID + data.Assets = convertAssetsToModel(w.AssetsStore.All(acc.Nickname, int(data.ChainID))) promptRequest := prompt.PromptRequest{ - Action: walletapp.Sign, - Data: data, + Action: walletapp.Sign, + Data: data, + PasswordRequired: true, } return &promptRequest, fees, nil @@ -288,13 +238,13 @@ func (w *walletSign) getPromptRequest(params operations.SignParams, acc *account func getCallSCPromptData( decodedMsg []byte, -) (PromptRequestSignData, error) { +) (prompt.PromptRequestSignData, error) { msg, err := callsc.DecodeMessage(decodedMsg) if err != nil { - return PromptRequestSignData{}, err + return prompt.PromptRequestSignData{}, err } - return PromptRequestSignData{ + return prompt.PromptRequestSignData{ Coins: strconv.FormatUint(msg.Coins, 10), Address: msg.Address, Function: msg.Function, @@ -304,25 +254,24 @@ func getCallSCPromptData( func getExecuteSCPromptData( decodedMsg []byte, -) (PromptRequestSignData, error) { +) (prompt.PromptRequestSignData, error) { msg, err := executesc.DecodeMessage(decodedMsg) if err != nil { - return PromptRequestSignData{}, err + return prompt.PromptRequestSignData{}, err } - promptReq := PromptRequestSignData{ + promptReq := prompt.PromptRequestSignData{ MaxCoins: strconv.FormatUint(msg.MaxCoins, 10), } // Check the datastore to know whether the ExecuteSC is a DeploySC or not - if msg.DataStore == nil { // the executeSC is not a deploySC return promptReq, nil } dataStore, err := onchain.DeSerializeDatastore(msg.DataStore) if err != nil { - return PromptRequestSignData{}, err + return prompt.PromptRequestSignData{}, err } deployedContract, isDeployDatastore := onchain.DatastoreToDeployedContract(dataStore) @@ -336,13 +285,13 @@ func getExecuteSCPromptData( func getRollPromptData( decodedMsg []byte, -) (PromptRequestSignData, error) { +) (prompt.PromptRequestSignData, error) { msg, err := sendoperation.RollDecodeMessage(decodedMsg) if err != nil { - return PromptRequestSignData{}, err + return prompt.PromptRequestSignData{}, err } - return PromptRequestSignData{ + return prompt.PromptRequestSignData{ RollCount: msg.RollCount, Coins: strconv.FormatUint(msg.RollCount*RollPrice, 10), }, nil @@ -350,10 +299,10 @@ func getRollPromptData( func (w *walletSign) getTransactionPromptData( decodedMsg []byte, -) (PromptRequestSignData, error) { +) (prompt.PromptRequestSignData, error) { msg, err := transaction.DecodeMessage(decodedMsg) if err != nil { - return PromptRequestSignData{}, err + return prompt.PromptRequestSignData{}, err } var recipientNickname string @@ -365,87 +314,13 @@ func (w *walletSign) getTransactionPromptData( recipientNickname = recipientAcc.Nickname } - return PromptRequestSignData{ + return prompt.PromptRequestSignData{ RecipientAddress: msg.RecipientAddress, RecipientNickname: recipientNickname, Amount: strconv.FormatUint(msg.Amount, 10), }, nil } -// privateKeyFromCache return the private key from the cache or an error. -func (w *walletSign) privateKeyFromCache( - acc *account.Account, -) (*memguard.LockedBuffer, error) { - cacheKey := cacheKeyPrefix + acc.Nickname - keyHash := KeyHash([]byte(cacheKey)) - - value, err := w.gc.Get(keyHash) - if err != nil { - if err.Error() == gcache.KeyNotFoundError.Error() { - return nil, fmt.Errorf("%w: %w", utils.ErrPrivateKeyCache, err) - } - - return nil, fmt.Errorf("%w: %w", utils.ErrCache, err) - } - - if value == nil { - return nil, fmt.Errorf("%w: %s", utils.ErrCache, "value is nil") - } - - byteValue, ok := value.([]byte) - if !ok { - return nil, fmt.Errorf("%w: %s", utils.ErrCache, "value is not a byte array") - } - - cacheValue := make([]byte, ed25519.PrivateKeySize) - copy(cacheValue, byteValue) - - cipheredPrivateKey := memguard.NewBufferFromBytes(cacheValue) - - privateKey, err := Xor(cipheredPrivateKey, keyHash) - if err != nil { - return nil, fmt.Errorf("%w: %w", utils.ErrCache, err) - } - - cipheredPrivateKey.Destroy() - - return privateKey, nil -} - -func Xor(pkey *memguard.LockedBuffer, cacheKeyHash [32]byte) (*memguard.LockedBuffer, error) { - a := pkey.Bytes() - - if len(a) != len(cacheKeyHash) { - return nil, fmt.Errorf("length of two arrays must be same, %d and %d", len(a), len(cacheKeyHash)) - } - result := make([]byte, len(a)) - - for i := 0; i < len(a); i++ { - result[i] = a[i] ^ cacheKeyHash[i] - } - - return memguard.NewBufferFromBytes(result), nil -} - -func KeyHash(cacheKeyHash []byte) [32]byte { - return blake3.Sum256(cacheKeyHash) -} - -func expirationDuration() time.Duration { - fromEnv := os.Getenv("BATCH_EXPIRATION_TIME") - - if fromEnv == "" { - return defaultExpirationTime - } - - duration, err := time.ParseDuration(fromEnv) - if err != nil { - return defaultExpirationTime - } - - return duration -} - func convertAssetsToModel(assetsWithBalance []*assets.AssetInfoWithBalances) []models.AssetInfo { result := make([]models.AssetInfo, 0) diff --git a/internal/handler/wallet/sign_message.go b/internal/handler/wallet/sign_message.go index 956534655..a357213cc 100644 --- a/internal/handler/wallet/sign_message.go +++ b/internal/handler/wallet/sign_message.go @@ -10,6 +10,7 @@ import ( "github.com/massalabs/station-massa-wallet/api/server/models" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" + "github.com/massalabs/station-massa-wallet/pkg/config" "github.com/massalabs/station-massa-wallet/pkg/prompt" "github.com/massalabs/station-massa-wallet/pkg/utils" "github.com/massalabs/station-massa-wallet/pkg/wallet/account" @@ -49,8 +50,7 @@ func (w *walletSignMessage) Handle(params operations.SignMessageParams) middlewa return newErrorResponse(err.Error(), errorSignDecodeMessage, http.StatusBadRequest) } - // Use the prompt-based logic to sign the message - promptOutput, err := prompt.WakeUpPrompt(w.prompterApp, *promptRequest, acc) + output, err := PromptForOperation(w.prompterApp, acc, promptRequest) if err != nil { msg := fmt.Sprintf("Unable to unprotect wallet: %s", err.Error()) if errors.Is(err, utils.ErrWrongPassword) || errors.Is(err, utils.ErrActionCanceled) { @@ -60,17 +60,7 @@ func (w *walletSignMessage) Handle(params operations.SignMessageParams) middlewa return newErrorResponse(msg, errorGetWallets, http.StatusInternalServerError) } - output, ok := promptOutput.(*walletapp.SignPromptOutput) - if !ok { - return newErrorResponse(fmt.Sprintf("prompting password for message: %v", utils.ErrInvalidInputType.Error()), utils.ErrInvalidInputType.Error(), http.StatusInternalServerError) - } - - w.prompterApp.EmitEvent(walletapp.PromptResultEvent, - walletapp.EventData{Success: true}) - - password := output.Password - - signature, err := acc.Sign(password, []byte(params.Body.Message)) + signature, err := acc.Sign(output.Password, []byte(params.Body.Message)) if err != nil { return newErrorResponse(fmt.Sprintf("unable to sign message: %s", err.Error()), errorGetWallets, http.StatusInternalServerError) } @@ -80,6 +70,15 @@ func (w *walletSignMessage) Handle(params operations.SignMessageParams) middlewa return newErrorResponse(err.Error(), errorGetAccount, http.StatusInternalServerError) } + cfg := config.Get() + + if cfg.HasEnabledRule(acc.Nickname) { + err = CachePrivateKey(w.gc, acc, output.Password) + if err != nil { + return newErrorResponse(err.Error(), errorCachePrivateKey, http.StatusInternalServerError) + } + } + // Return the signature and public key as the response return operations.NewSignMessageOK().WithPayload( &models.SignResponse{ diff --git a/internal/handler/wallet/sign_rule_add.go b/internal/handler/wallet/sign_rule_add.go new file mode 100644 index 000000000..9938b0637 --- /dev/null +++ b/internal/handler/wallet/sign_rule_add.go @@ -0,0 +1,110 @@ +package wallet + +import ( + "fmt" + "net/http" + + "github.com/bluele/gcache" + "github.com/go-openapi/runtime/middleware" + "github.com/massalabs/station-massa-wallet/api/server/models" + "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" + walletapp "github.com/massalabs/station-massa-wallet/pkg/app" + "github.com/massalabs/station-massa-wallet/pkg/config" + "github.com/massalabs/station-massa-wallet/pkg/prompt" + "github.com/massalabs/station-massa-wallet/pkg/utils" + "github.com/massalabs/station-massa-wallet/pkg/wallet/account" + "github.com/pkg/errors" +) + +type SignRulePromptData struct { + Nickname string + WalletAddress string + Description string + SignRule config.SignRule +} + +func NewAddSignRuleHandler(prompterApp prompt.WalletPrompterInterface, gc gcache.Cache) operations.AddSignRuleHandler { + return &addSignRuleHandler{gc: gc, prompterApp: prompterApp} +} + +type addSignRuleHandler struct { + prompterApp prompt.WalletPrompterInterface + gc gcache.Cache +} + +func (w *addSignRuleHandler) Handle(params operations.AddSignRuleParams) middleware.Responder { + acc, errResp := loadAccount(w.prompterApp.App().Wallet, params.Nickname) + if errResp != nil { + return errResp + } + + promptRequest, err := w.getPromptRequest(params, acc) + if err != nil { + return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorAddSignRule, http.StatusBadRequest) + } + + password, err := PromptPassword(w.prompterApp, acc, promptRequest) + if err != nil { + msg := fmt.Sprintf("Unable to unprotect wallet: %s", err.Error()) + if errors.Is(err, utils.ErrWrongPassword) || errors.Is(err, utils.ErrActionCanceled) { + return newErrorResponse(msg, errorGetWallets, http.StatusUnauthorized) + } + + return newErrorResponse(msg, errorGetWallets, http.StatusInternalServerError) + } + + cfg := config.Get() + + newRule := config.SignRule{ + Name: params.Body.Name, + Contract: *params.Body.Contract, + RuleType: config.RuleType(params.Body.RuleType), + Enabled: *params.Body.Enabled, + } + + ruleID, err := cfg.AddSignRule(acc.Nickname, newRule) + if err != nil { + return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorAddSignRule, http.StatusInternalServerError) + } + + if cfg.HasEnabledRule(acc.Nickname) { + err = CachePrivateKeyFromPassword(w.gc, acc, password) + if err != nil { + return newErrorResponse(err.Error(), errorCachePrivateKey, http.StatusInternalServerError) + } + } + + return w.Success(ruleID) +} + +func (w *addSignRuleHandler) Success(ruleID string) middleware.Responder { + return operations.NewAddSignRuleOK().WithPayload( + &models.AddSignRuleResponse{ + ID: ruleID, + }) +} + +func (w *addSignRuleHandler) getPromptRequest(params operations.AddSignRuleParams, acc *account.Account) (*prompt.PromptRequest, error) { + address, err := acc.Address.String() + if err != nil { + return nil, fmt.Errorf("failed to stringify address: %w", err) + } + + promptData := SignRulePromptData{ + WalletAddress: address, + Nickname: acc.Nickname, + Description: params.Body.Description, + SignRule: config.SignRule{ + Contract: *params.Body.Contract, + RuleType: config.RuleType(params.Body.RuleType), + Enabled: *params.Body.Enabled, + }, + } + + promptRequest := prompt.PromptRequest{ + Action: walletapp.AddSignRule, + Data: promptData, + } + + return &promptRequest, nil +} diff --git a/internal/handler/wallet/sign_rule_delete.go b/internal/handler/wallet/sign_rule_delete.go new file mode 100644 index 000000000..5bc9d0de0 --- /dev/null +++ b/internal/handler/wallet/sign_rule_delete.go @@ -0,0 +1,85 @@ +package wallet + +import ( + "fmt" + "net/http" + + "github.com/bluele/gcache" + "github.com/go-openapi/runtime/middleware" + "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" + walletapp "github.com/massalabs/station-massa-wallet/pkg/app" + "github.com/massalabs/station-massa-wallet/pkg/config" + "github.com/massalabs/station-massa-wallet/pkg/prompt" + "github.com/massalabs/station-massa-wallet/pkg/utils" + "github.com/massalabs/station-massa-wallet/pkg/wallet/account" + "github.com/pkg/errors" +) + +func NewDeleteSignRuleHandler(prompterApp prompt.WalletPrompterInterface, gc gcache.Cache) operations.DeleteSignRuleHandler { + return &deleteSignRuleHandler{gc: gc, prompterApp: prompterApp} +} + +type deleteSignRuleHandler struct { + prompterApp prompt.WalletPrompterInterface + gc gcache.Cache +} + +func (w *deleteSignRuleHandler) Handle(params operations.DeleteSignRuleParams) middleware.Responder { + acc, errResp := loadAccount(w.prompterApp.App().Wallet, params.Nickname) + if errResp != nil { + return errResp + } + + cfg := config.Get() + + signRule, err := cfg.GetSignRule(acc.Nickname, params.RuleID) + if err != nil { + return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorDeleteSignRule, http.StatusInternalServerError) + } + + promptRequest, err := w.getPromptRequest(acc, signRule) + if err != nil { + return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorDeleteSignRule, http.StatusBadRequest) + } + + _, err = PromptPassword(w.prompterApp, acc, promptRequest) + if err != nil { + msg := fmt.Sprintf("Unable to unprotect wallet: %s", err.Error()) + if errors.Is(err, utils.ErrWrongPassword) || errors.Is(err, utils.ErrActionCanceled) { + return newErrorResponse(msg, errorGetWallets, http.StatusUnauthorized) + } + + return newErrorResponse(msg, errorGetWallets, http.StatusInternalServerError) + } + + err = cfg.DeleteSignRule(acc.Nickname, params.RuleID) + if err != nil { + return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorDeleteSignRule, http.StatusInternalServerError) + } + + return w.Success() +} + +func (w *deleteSignRuleHandler) Success() middleware.Responder { + return operations.NewDeleteSignRuleOK() +} + +func (w *deleteSignRuleHandler) getPromptRequest(acc *account.Account, signRule *config.SignRule) (*prompt.PromptRequest, error) { + address, err := acc.Address.String() + if err != nil { + return nil, fmt.Errorf("failed to stringify address: %w", err) + } + + promptData := SignRulePromptData{ + WalletAddress: address, + Nickname: acc.Nickname, + SignRule: *signRule, + } + + promptRequest := prompt.PromptRequest{ + Action: walletapp.DeleteSignRule, + Data: promptData, + } + + return &promptRequest, nil +} diff --git a/internal/handler/wallet/sign_rule_test.go b/internal/handler/wallet/sign_rule_test.go new file mode 100644 index 000000000..ad0dd5c04 --- /dev/null +++ b/internal/handler/wallet/sign_rule_test.go @@ -0,0 +1,107 @@ +package wallet + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/massalabs/station-massa-wallet/api/server/models" + walletapp "github.com/massalabs/station-massa-wallet/pkg/app" + assertLib "github.com/stretchr/testify/assert" +) + +func Test_signrule_Handlers(t *testing.T) { + assert := assertLib.New(t) + + api, resChan, err := MockAPI() + assert.NoError(err) + + addhandler, exist := api.HandlerFor("post", "/api/accounts/{nickname}/signrules") + assert.True(exist) + + nickname := "zeWahLetName" + password := "zePassword" + account := createAccount(password, nickname, t, prompterAppMock) + + t.Run("Add sign rule", func(t *testing.T) { + body := `{ + "name": "Test Rule", + "contract": "Test Contract", + "ruleType": "auto_sign", + "enabled": true, + "description": "Test Description" + }` + + testResult := make(chan walletapp.EventData) + + // Send password to prompter app and wait for result + go func(res chan walletapp.EventData) { + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ + BaseMessage: walletapp.BaseMessage{}, + Message: password, + } + + // forward test result to test goroutine + res <- (<-resChan) + }(testResult) + + resp, err := handleHTTPRequest(addhandler, "POST", fmt.Sprintf("/api/accounts/%s/signrules", nickname), body) + assert.NoError(err) + fmt.Println(resp) + + verifyStatusCode(t, resp, http.StatusOK) + + result := <-testResult + + checkResultChannel(t, result, true, "") + + var addRuleResponse models.AddSignRuleResponse + err = json.Unmarshal(resp.Body.Bytes(), &addRuleResponse) + assert.NoError(err) + assert.NotEmpty(addRuleResponse.ID) + + // check that privateKey is cached + pkey, err := privateKeyFromCache(testCache, account) + assert.NoError(err) + assert.NotNil(pkey) + }) +} + +// func TestDeleteSignRule(t *testing.T) { +// api, _, _, resultChannel, err := MockAPI() +// assert.NoError(t, err) + +// body := `{ +// "nickname": "Test Nickname", +// "ruleID": "Test Rule ID" +// }` + +// resp, err := processHTTPRequest(api, "DELETE", "/sign-rule", body) +// assert.NoError(t, err) +// assert.Equal(t, http.StatusOK, resp.Code) + +// result := <-resultChannel +// checkResultChannel(t, result, true, "Sign rule deleted successfully") +// } + +// func TestUpdateSignRule(t *testing.T) { +// api, _, _, resultChannel, err := MockAPI() +// assert.NoError(t, err) + +// body := `{ +// "nickname": "Test Nickname", +// "ruleID": "Test Rule ID", +// "name": "Updated Rule", +// "contract": "Updated Contract", +// "ruleType": "Updated Type", +// "enabled": false +// }` + +// resp, err := processHTTPRequest(api, "PUT", "/sign-rule", body) +// assert.NoError(t, err) +// assert.Equal(t, http.StatusOK, resp.Code) + +// result := <-resultChannel +// checkResultChannel(t, result, true, "Sign rule updated successfully") +// } diff --git a/internal/handler/wallet/sign_rule_update.go b/internal/handler/wallet/sign_rule_update.go new file mode 100644 index 000000000..ca3e703ed --- /dev/null +++ b/internal/handler/wallet/sign_rule_update.go @@ -0,0 +1,103 @@ +package wallet + +import ( + "fmt" + "net/http" + + "github.com/bluele/gcache" + "github.com/go-openapi/runtime/middleware" + "github.com/massalabs/station-massa-wallet/api/server/models" + "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" + walletapp "github.com/massalabs/station-massa-wallet/pkg/app" + "github.com/massalabs/station-massa-wallet/pkg/config" + "github.com/massalabs/station-massa-wallet/pkg/prompt" + "github.com/massalabs/station-massa-wallet/pkg/utils" + "github.com/massalabs/station-massa-wallet/pkg/wallet/account" + "github.com/pkg/errors" +) + +func NewUpdateSignRuleHandler(prompterApp prompt.WalletPrompterInterface, gc gcache.Cache) operations.UpdateSignRuleHandler { + return &updateSignRuleHandler{gc: gc, prompterApp: prompterApp} +} + +type updateSignRuleHandler struct { + prompterApp prompt.WalletPrompterInterface + gc gcache.Cache +} + +func (w *updateSignRuleHandler) Handle(params operations.UpdateSignRuleParams) middleware.Responder { + acc, errResp := loadAccount(w.prompterApp.App().Wallet, params.Nickname) + if errResp != nil { + return errResp + } + + cfg := config.Get() + + signRule, err := cfg.GetSignRule(acc.Nickname, params.RuleID) + if err != nil { + return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorUpdateSignRule, http.StatusInternalServerError) + } + + promptRequest, err := w.getPromptRequest(acc, signRule) + if err != nil { + return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorUpdateSignRule, http.StatusBadRequest) + } + + password, err := PromptPassword(w.prompterApp, acc, promptRequest) + if err != nil { + msg := fmt.Sprintf("Unable to unprotect wallet: %s", err.Error()) + if errors.Is(err, utils.ErrWrongPassword) || errors.Is(err, utils.ErrActionCanceled) { + return newErrorResponse(msg, errorGetWallets, http.StatusUnauthorized) + } + + return newErrorResponse(msg, errorGetWallets, http.StatusInternalServerError) + } + + newRule := config.SignRule{ + Name: params.Body.Name, + Contract: *params.Body.Contract, + RuleType: config.RuleType(params.Body.RuleType), + Enabled: *params.Body.Enabled, + } + + ruleID, err := cfg.UpdateSignRule(acc.Nickname, params.RuleID, newRule) + if err != nil { + return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorUpdateSignRule, http.StatusInternalServerError) + } + + if cfg.HasEnabledRule(acc.Nickname) { + err = CachePrivateKeyFromPassword(w.gc, acc, password) + if err != nil { + return newErrorResponse(err.Error(), errorCachePrivateKey, http.StatusInternalServerError) + } + } + + return w.Success(ruleID) +} + +func (w *updateSignRuleHandler) Success(ruleID string) middleware.Responder { + return operations.NewUpdateSignRuleOK().WithPayload( + &models.UpdateSignRuleResponse{ + ID: ruleID, + }) +} + +func (w *updateSignRuleHandler) getPromptRequest(acc *account.Account, signRule *config.SignRule) (*prompt.PromptRequest, error) { + address, err := acc.Address.String() + if err != nil { + return nil, fmt.Errorf("failed to stringify address: %w", err) + } + + promptData := SignRulePromptData{ + WalletAddress: address, + Nickname: acc.Nickname, + SignRule: *signRule, + } + + promptRequest := prompt.PromptRequest{ + Action: walletapp.UpdateSignRule, + Data: promptData, + } + + return &promptRequest, nil +} diff --git a/internal/handler/wallet/sign_test.go b/internal/handler/wallet/sign_test.go index 59525db7c..2328b5b74 100644 --- a/internal/handler/wallet/sign_test.go +++ b/internal/handler/wallet/sign_test.go @@ -1,20 +1,25 @@ package wallet import ( + "encoding/json" "fmt" "net/http" "net/http/httptest" "strconv" "testing" + "github.com/massalabs/station-massa-wallet/api/server/models" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" + "github.com/massalabs/station-massa-wallet/pkg/config" "github.com/massalabs/station-massa-wallet/pkg/utils" - "github.com/massalabs/station-massa-wallet/pkg/wallet/account" "github.com/stretchr/testify/assert" ) -const callSCString = "AKT4CASAzuTNAqCNBgEAXBwUw39NBQYix8Ovph0TUiJuDDEnlFYUPgsbeMbrA4cLZm9yd2FyZEJ1cm7FAQDgfY7fLW7qpwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAAAweGZDRERBRTI1MTAwNjIxYTViQzg4MTlkQzlEMzg0MjUzNEQ3QmY0NzYAAAAANQAAAEFTMTJUUm9TY01kd0xLOFlwdDZOQkFwcHl6Q0Z3N1FlRzVlM3hGdnhwQ0FuQW5ZTGZ1TVVUKgAAADB4NTM4NDRGOTU3N0MyMzM0ZTU0MUFlYzdEZjcxNzRFQ2U1ZEYxZkNmMKc2qgAAAAAA" +const ( + callSCString = "AKT4CASAzuTNAqCNBgEAXBwUw39NBQYix8Ovph0TUiJuDDEnlFYUPgsbeMbrA4cLZm9yd2FyZEJ1cm7FAQDgfY7fLW7qpwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAAAweGZDRERBRTI1MTAwNjIxYTViQzg4MTlkQzlEMzg0MjUzNEQ3QmY0NzYAAAAANQAAAEFTMTJUUm9TY01kd0xLOFlwdDZOQkFwcHl6Q0Z3N1FlRzVlM3hGdnhwQ0FuQW5ZTGZ1TVVUKgAAADB4NTM4NDRGOTU3N0MyMzM0ZTU0MUFlYzdEZjcxNzRFQ2U1ZEYxZkNmMKc2qgAAAAAA" + contract = "AS1hZpUH6TPiRxHtTKqAfXDmZ7Afa7UfS4rtYN7NxVwAaSAphCET" +) func signTransaction(t *testing.T, api *operations.MassaWalletAPI, nickname string, body string) *httptest.ResponseRecorder { handler, exist := api.HandlerFor("post", "/api/accounts/{nickname}/sign") @@ -36,27 +41,23 @@ func signMessage(t *testing.T, api *operations.MassaWalletAPI, nickname string, return resp } -func TestPrepareOperation(t *testing.T) { - acc := account.NewAccount(t) - fees := uint64(1000) - operationB64 := "AKT4CASAzuTNAqCNBgEAXBwUw39NBQYix8Ovph0TUiJuDDEnlFYUPgsbeMbrA4cLZm9yd2FyZEJ1cm7FAQDgfY7fLW7qpwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAAAweGZDRERBRTI1MTAwNjIxYTViQzg4MTlkQzlEMzg0MjUzNEQ3QmY0NzYAAAAANQAAAEFTMTJUUm9TY01kd0xLOFlwdDZOQkFwcHl6Q0Z3N1FlRzVlM3hGdnhwQ0FuQW5ZTGZ1TVVUKgAAADB4NTM4NDRGOTU3N0MyMzM0ZTU0MUFlYzdEZjcxNzRFQ2U1ZEYxZkNmMKc2qgAAAAAA" - - operation, msgToSign, err := prepareOperation(acc, fees, operationB64, ChainIDUnitTests) +func verifySignResponse(t *testing.T, resp *httptest.ResponseRecorder) { + var signResponse models.SignResponse + err := json.Unmarshal(resp.Body.Bytes(), &signResponse) assert.NoError(t, err) - expected := []byte{0xe8, 0x7, 0xa4, 0xf8, 0x8, 0x4, 0x80, 0xce, 0xe4, 0xcd, 0x2, 0xa0, 0x8d, 0x6, 0x1, 0x0, 0x5c, 0x1c, 0x14, 0xc3, 0x7f, 0x4d, 0x5, 0x6, 0x22, 0xc7, 0xc3, 0xaf, 0xa6, 0x1d, 0x13, 0x52, 0x22, 0x6e, 0xc, 0x31, 0x27, 0x94, 0x56, 0x14, 0x3e, 0xb, 0x1b, 0x78, 0xc6, 0xeb, 0x3, 0x87, 0xb, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x42, 0x75, 0x72, 0x6e, 0xc5, 0x1, 0x0, 0xe0, 0x7d, 0x8e, 0xdf, 0x2d, 0x6e, 0xea, 0xa7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2a, 0x0, 0x0, 0x0, 0x30, 0x78, 0x66, 0x43, 0x44, 0x44, 0x41, 0x45, 0x32, 0x35, 0x31, 0x30, 0x30, 0x36, 0x32, 0x31, 0x61, 0x35, 0x62, 0x43, 0x38, 0x38, 0x31, 0x39, 0x64, 0x43, 0x39, 0x44, 0x33, 0x38, 0x34, 0x32, 0x35, 0x33, 0x34, 0x44, 0x37, 0x42, 0x66, 0x34, 0x37, 0x36, 0x0, 0x0, 0x0, 0x0, 0x35, 0x0, 0x0, 0x0, 0x41, 0x53, 0x31, 0x32, 0x54, 0x52, 0x6f, 0x53, 0x63, 0x4d, 0x64, 0x77, 0x4c, 0x4b, 0x38, 0x59, 0x70, 0x74, 0x36, 0x4e, 0x42, 0x41, 0x70, 0x70, 0x79, 0x7a, 0x43, 0x46, 0x77, 0x37, 0x51, 0x65, 0x47, 0x35, 0x65, 0x33, 0x78, 0x46, 0x76, 0x78, 0x70, 0x43, 0x41, 0x6e, 0x41, 0x6e, 0x59, 0x4c, 0x66, 0x75, 0x4d, 0x55, 0x54, 0x2a, 0x0, 0x0, 0x0, 0x30, 0x78, 0x35, 0x33, 0x38, 0x34, 0x34, 0x46, 0x39, 0x35, 0x37, 0x37, 0x43, 0x32, 0x33, 0x33, 0x34, 0x65, 0x35, 0x34, 0x31, 0x41, 0x65, 0x63, 0x37, 0x44, 0x66, 0x37, 0x31, 0x37, 0x34, 0x45, 0x43, 0x65, 0x35, 0x64, 0x46, 0x31, 0x66, 0x43, 0x66, 0x30, 0xa7, 0x36, 0xaa, 0x0, 0x0, 0x0, 0x0, 0x0} - assert.Equal(t, expected, operation) - expectedMsg := []byte{0x0, 0x0, 0x0, 0x0, 0x4, 0xa0, 0xf8, 0xfe, 0x0, 0x2d, 0x96, 0xbc, 0xda, 0xcb, 0xbe, 0x41, 0x38, 0x2c, 0xa2, 0x3e, 0x52, 0xe3, 0xd2, 0x19, 0x6c, 0xba, 0x65, 0xe7, 0xa1, 0xac, 0xd2, 0x9, 0xdf, 0xc9, 0x5c, 0x6b, 0x32, 0xb6, 0xa1, 0x8a, 0x93, 0xe8, 0x7, 0xa4, 0xf8, 0x8, 0x4, 0x80, 0xce, 0xe4, 0xcd, 0x2, 0xa0, 0x8d, 0x6, 0x1, 0x0, 0x5c, 0x1c, 0x14, 0xc3, 0x7f, 0x4d, 0x5, 0x6, 0x22, 0xc7, 0xc3, 0xaf, 0xa6, 0x1d, 0x13, 0x52, 0x22, 0x6e, 0xc, 0x31, 0x27, 0x94, 0x56, 0x14, 0x3e, 0xb, 0x1b, 0x78, 0xc6, 0xeb, 0x3, 0x87, 0xb, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x42, 0x75, 0x72, 0x6e, 0xc5, 0x1, 0x0, 0xe0, 0x7d, 0x8e, 0xdf, 0x2d, 0x6e, 0xea, 0xa7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2a, 0x0, 0x0, 0x0, 0x30, 0x78, 0x66, 0x43, 0x44, 0x44, 0x41, 0x45, 0x32, 0x35, 0x31, 0x30, 0x30, 0x36, 0x32, 0x31, 0x61, 0x35, 0x62, 0x43, 0x38, 0x38, 0x31, 0x39, 0x64, 0x43, 0x39, 0x44, 0x33, 0x38, 0x34, 0x32, 0x35, 0x33, 0x34, 0x44, 0x37, 0x42, 0x66, 0x34, 0x37, 0x36, 0x0, 0x0, 0x0, 0x0, 0x35, 0x0, 0x0, 0x0, 0x41, 0x53, 0x31, 0x32, 0x54, 0x52, 0x6f, 0x53, 0x63, 0x4d, 0x64, 0x77, 0x4c, 0x4b, 0x38, 0x59, 0x70, 0x74, 0x36, 0x4e, 0x42, 0x41, 0x70, 0x70, 0x79, 0x7a, 0x43, 0x46, 0x77, 0x37, 0x51, 0x65, 0x47, 0x35, 0x65, 0x33, 0x78, 0x46, 0x76, 0x78, 0x70, 0x43, 0x41, 0x6e, 0x41, 0x6e, 0x59, 0x4c, 0x66, 0x75, 0x4d, 0x55, 0x54, 0x2a, 0x0, 0x0, 0x0, 0x30, 0x78, 0x35, 0x33, 0x38, 0x34, 0x34, 0x46, 0x39, 0x35, 0x37, 0x37, 0x43, 0x32, 0x33, 0x33, 0x34, 0x65, 0x35, 0x34, 0x31, 0x41, 0x65, 0x63, 0x37, 0x44, 0x66, 0x37, 0x31, 0x37, 0x34, 0x45, 0x43, 0x65, 0x35, 0x64, 0x46, 0x31, 0x66, 0x43, 0x66, 0x30, 0xa7, 0x36, 0xaa, 0x0, 0x0, 0x0, 0x0, 0x0} - assert.Equal(t, expectedMsg, msgToSign) + assert.NotEmpty(t, signResponse.Signature) + assert.NotEmpty(t, signResponse.Operation) + assert.NotEmpty(t, signResponse.PublicKey) } func Test_walletSign_Handle(t *testing.T) { - api, prompterApp, _, resChan, err := MockAPI() + api, resChan, err := MockAPI() assert.NoError(t, err) transactionData := fmt.Sprintf(`{"chainId": `+strconv.FormatUint(ChainIDUnitTests, 10)+`, "operation":"%s"}`, callSCString) nickname := "walletToDelete" password := "zePassword" - createAccount(password, nickname, t, prompterApp) + account := createAccount(password, nickname, t, prompterAppMock) t.Run("invalid nickname", func(t *testing.T) { resp := signTransaction(t, api, "Johnny", transactionData) @@ -68,7 +69,7 @@ func Test_walletSign_Handle(t *testing.T) { // Send password to prompter app and wait for result go func(res chan walletapp.EventData) { - prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ BaseMessage: walletapp.BaseMessage{}, Password: password, Fees: "14400", @@ -83,6 +84,7 @@ func Test_walletSign_Handle(t *testing.T) { result := <-testResult checkResultChannel(t, result, true, "") + verifySignResponse(t, resp) }) t.Run("sign a plain text message", func(t *testing.T) { @@ -90,7 +92,7 @@ func Test_walletSign_Handle(t *testing.T) { // Send password to prompter app and wait for result go func(res chan walletapp.EventData) { - prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ BaseMessage: walletapp.BaseMessage{}, Password: password, Fees: "1000", @@ -108,6 +110,12 @@ func Test_walletSign_Handle(t *testing.T) { result := <-testResult checkResultChannel(t, result, true, "") + + var signResponse models.SignResponse + err := json.Unmarshal(resp.Body.Bytes(), &signResponse) + assert.NoError(t, err) + assert.NotEmpty(t, signResponse.Signature) + assert.NotEmpty(t, signResponse.PublicKey) }) // The handler will not return until a the good password is sent or the action is canceled @@ -117,7 +125,7 @@ func Test_walletSign_Handle(t *testing.T) { //nolint:staticcheck go func(res chan walletapp.EventData) { // Send wrong password to prompter app and wait for result - prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ BaseMessage: walletapp.BaseMessage{}, Password: "this is not the password", Fees: "1000", @@ -128,7 +136,7 @@ func Test_walletSign_Handle(t *testing.T) { checkResultChannel(t, failRes, false, utils.WrongPassword) // Send password to prompter app to unlock the handler - prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ BaseMessage: walletapp.BaseMessage{}, Password: password, Fees: "1000", @@ -144,13 +152,14 @@ func Test_walletSign_Handle(t *testing.T) { result := <-testResult checkResultChannel(t, result, true, "") + verifySignResponse(t, resp) }) t.Run("invalid password try, then action canceled by user", func(t *testing.T) { //nolint:staticcheck go func() { // Send wrong password to prompter app and wait for result - prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ BaseMessage: walletapp.BaseMessage{}, Password: "this is not the password", Fees: "1000", @@ -161,10 +170,319 @@ func Test_walletSign_Handle(t *testing.T) { checkResultChannel(t, failRes, false, utils.WrongPassword) // Send cancel to prompter app to unlock the handler - prompterApp.App().CtrlChan <- walletapp.Cancel + prompterAppMock.App().CtrlChan <- walletapp.Cancel }() resp := signTransaction(t, api, nickname, transactionData) verifyStatusCode(t, resp, http.StatusUnauthorized) }) + + t.Run("Auto Sign", func(t *testing.T) { + cfg := config.Get() + ruleId, err := cfg.AddSignRule(nickname, config.SignRule{ + Name: "test", + Contract: contract, + RuleType: config.RuleTypeAutoSign, + Enabled: true, + }) + + assert.NoError(t, err) + + assert.True(t, cfg.HasEnabledRule(nickname)) + + testResult := make(chan walletapp.EventData) + + // Send password to prompter app and wait for result + go func(res chan walletapp.EventData) { + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ + BaseMessage: walletapp.BaseMessage{}, + Password: password, + Fees: "14400", + } + // forward test result to test goroutine + res <- (<-resChan) + }(testResult) + + resp := signTransaction(t, api, nickname, transactionData) + verifyStatusCode(t, resp, http.StatusOK) + + result := <-testResult + + checkResultChannel(t, result, true, "") + + // check that privateKey is cached + pkey, err := privateKeyFromCache(testCache, account) + assert.NoError(t, err) + assert.NotNil(t, pkey) + + // sign again, should not prompt for password + resp = signTransaction(t, api, nickname, transactionData) + verifyStatusCode(t, resp, http.StatusOK) + + verifySignResponse(t, resp) + + err = cfg.DeleteSignRule(nickname, ruleId) + assert.NoError(t, err) + }) + + t.Run("Auto Sign with enabled rule", func(t *testing.T) { + // Clean cache + testCache.Purge() + + cfg := config.Get() + ruleId, err := cfg.AddSignRule(nickname, config.SignRule{ + Name: "test", + Contract: "AnotherContract", + RuleType: config.RuleTypeAutoSign, + Enabled: true, + }) + + assert.NoError(t, err) + + assert.True(t, cfg.HasEnabledRule(nickname)) + + testResult := make(chan walletapp.EventData) + + // Send password to prompter app and wait for result + go func(res chan walletapp.EventData) { + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ + BaseMessage: walletapp.BaseMessage{}, + Password: password, + Fees: "14400", + } + // forward test result to test goroutine + res <- (<-resChan) + }(testResult) + + resp := signTransaction(t, api, nickname, transactionData) + verifyStatusCode(t, resp, http.StatusOK) + + result := <-testResult + + checkResultChannel(t, result, true, "") + + // check that privateKey is cached + pkey, err := privateKeyFromCache(testCache, account) + assert.NoError(t, err) + assert.NotNil(t, pkey) + + // sign again, should prompt for password + go func(res chan walletapp.EventData) { + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ + BaseMessage: walletapp.BaseMessage{}, + Password: password, + Fees: "14400", + } + // forward test result to test goroutine + res <- (<-resChan) + }(testResult) + + resp = signTransaction(t, api, nickname, transactionData) + verifyStatusCode(t, resp, http.StatusOK) + + result = <-testResult + + checkResultChannel(t, result, true, "") + + verifySignResponse(t, resp) + + err = cfg.DeleteSignRule(nickname, ruleId) + assert.NoError(t, err) + }) + + t.Run("Disable Password Prompt for contract", func(t *testing.T) { + // Clean cache + testCache.Purge() + + cfg := config.Get() + ruleId, err := cfg.AddSignRule(nickname, config.SignRule{ + Name: "test", + Contract: contract, + RuleType: config.RuleTypeDisablePasswordPrompt, + Enabled: true, + }) + + assert.NoError(t, err) + + assert.True(t, cfg.HasEnabledRule(nickname)) + + testResult := make(chan walletapp.EventData) + + // Send password to prompter app and wait for result + go func(res chan walletapp.EventData) { + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ + BaseMessage: walletapp.BaseMessage{}, + Password: password, + Fees: "14400", + } + // forward test result to test goroutine + res <- (<-resChan) + }(testResult) + + fmt.Println(">>>>Sign transaction password needed") + resp := signTransaction(t, api, nickname, transactionData) + verifyStatusCode(t, resp, http.StatusOK) + + result := <-testResult + + checkResultChannel(t, result, true, "") + + // check that privateKey is cached + pkey, err := privateKeyFromCache(testCache, account) + assert.NoError(t, err) + assert.NotNil(t, pkey) + + // sign again, should prompt for user confirmation, no password needed + go func(res chan walletapp.EventData) { + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ + BaseMessage: walletapp.BaseMessage{}, + Fees: "12345", + } + // forward test result to test goroutine + res <- (<-resChan) + }(testResult) + + fmt.Println(">>>>Sign transaction password NOT needed") + + resp = signTransaction(t, api, nickname, transactionData) + verifyStatusCode(t, resp, http.StatusOK) + + result = <-testResult + + checkResultChannel(t, result, true, "") + + verifySignResponse(t, resp) + + err = cfg.DeleteSignRule(nickname, ruleId) + assert.NoError(t, err) + }) + + t.Run("Disable Password Prompt enabled for another contract", func(t *testing.T) { + // Clean cache + testCache.Purge() + + cfg := config.Get() + ruleId, err := cfg.AddSignRule(nickname, config.SignRule{ + Name: "test", + Contract: "DummyContract", + RuleType: config.RuleTypeDisablePasswordPrompt, + Enabled: true, + }) + + assert.NoError(t, err) + + assert.True(t, cfg.HasEnabledRule(nickname)) + + testResult := make(chan walletapp.EventData) + + // Send password to prompter app and wait for result + go func(res chan walletapp.EventData) { + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ + BaseMessage: walletapp.BaseMessage{}, + Password: password, + Fees: "14400", + } + // forward test result to test goroutine + res <- (<-resChan) + }(testResult) + + resp := signTransaction(t, api, nickname, transactionData) + verifyStatusCode(t, resp, http.StatusOK) + + result := <-testResult + + checkResultChannel(t, result, true, "") + + // check that privateKey is cached + pkey, err := privateKeyFromCache(testCache, account) + assert.NoError(t, err) + assert.NotNil(t, pkey) + + // sign again, should prompt for user confirmation, password is needed + go func(res chan walletapp.EventData) { + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ + BaseMessage: walletapp.BaseMessage{}, + Password: password, + Fees: "14400", + } + // forward test result to test goroutine + res <- (<-resChan) + }(testResult) + + resp = signTransaction(t, api, nickname, transactionData) + verifyStatusCode(t, resp, http.StatusOK) + + result = <-testResult + + checkResultChannel(t, result, true, "") + + verifySignResponse(t, resp) + + err = cfg.DeleteSignRule(nickname, ruleId) + assert.NoError(t, err) + }) + + t.Run("Disable Password Prompt enabled for all contract.", func(t *testing.T) { + // Clean cache + testCache.Purge() + + cfg := config.Get() + ruleId, err := cfg.AddSignRule(nickname, config.SignRule{ + Name: "test", + Contract: "*", + RuleType: config.RuleTypeDisablePasswordPrompt, + Enabled: true, + }) + + assert.NoError(t, err) + + assert.True(t, cfg.HasEnabledRule(nickname)) + + testResult := make(chan walletapp.EventData) + + // Send password to prompter app and wait for result + go func(res chan walletapp.EventData) { + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ + BaseMessage: walletapp.BaseMessage{}, + Password: password, + Fees: "14400", + } + // forward test result to test goroutine + res <- (<-resChan) + }(testResult) + + resp := signTransaction(t, api, nickname, transactionData) + verifyStatusCode(t, resp, http.StatusOK) + + result := <-testResult + + checkResultChannel(t, result, true, "") + + // check that privateKey is cached + pkey, err := privateKeyFromCache(testCache, account) + assert.NoError(t, err) + assert.NotNil(t, pkey) + + // sign again, should prompt for user confirmation, password is not needed + go func(res chan walletapp.EventData) { + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ + BaseMessage: walletapp.BaseMessage{}, + Fees: "14400", + } + // forward test result to test goroutine + res <- (<-resChan) + }(testResult) + + resp = signTransaction(t, api, nickname, transactionData) + verifyStatusCode(t, resp, http.StatusOK) + + result = <-testResult + + checkResultChannel(t, result, true, "") + + verifySignResponse(t, resp) + + err = cfg.DeleteSignRule(nickname, ruleId) + assert.NoError(t, err) + }) } diff --git a/internal/handler/wallet/transfer.go b/internal/handler/wallet/transfer.go index bfbb13409..44abf93a2 100644 --- a/internal/handler/wallet/transfer.go +++ b/internal/handler/wallet/transfer.go @@ -6,10 +6,12 @@ import ( "strconv" "github.com/awnumar/memguard" + "github.com/bluele/gcache" "github.com/go-openapi/runtime/middleware" "github.com/massalabs/station-massa-wallet/api/server/models" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" + "github.com/massalabs/station-massa-wallet/pkg/config" "github.com/massalabs/station-massa-wallet/pkg/network" "github.com/massalabs/station-massa-wallet/pkg/prompt" "github.com/massalabs/station-massa-wallet/pkg/utils" @@ -18,13 +20,14 @@ import ( "github.com/massalabs/station/pkg/node/sendoperation/transaction" ) -func NewTransferCoin(prompterApp prompt.WalletPrompterInterface, massaClient network.NodeFetcherInterface) operations.TransferCoinHandler { - return &transferCoin{prompterApp: prompterApp, massaClient: massaClient} +func NewTransferCoin(prompterApp prompt.WalletPrompterInterface, massaClient network.NodeFetcherInterface, gc gcache.Cache) operations.TransferCoinHandler { + return &transferCoin{prompterApp: prompterApp, massaClient: massaClient, gc: gc} } type transferCoin struct { prompterApp prompt.WalletPrompterInterface massaClient network.NodeFetcherInterface + gc gcache.Cache } func (t *transferCoin) Handle(params operations.TransferCoinParams) middleware.Responder { @@ -68,7 +71,7 @@ func (t *transferCoin) Handle(params operations.TransferCoinParams) middleware.R promptRequest := prompt.PromptRequest{ Action: walletapp.Sign, - Data: PromptRequestSignData{ + Data: prompt.PromptRequestSignData{ Fees: strconv.FormatUint(fee, 10), MinFees: minimalFees, WalletAddress: address, @@ -107,6 +110,15 @@ func (t *transferCoin) Handle(params operations.TransferCoinParams) middleware.R t.prompterApp.EmitEvent(walletapp.PromptResultEvent, walletapp.EventData{Success: true}) + cfg := config.Get() + + if cfg.HasEnabledRule(acc.Nickname) { + err = CachePrivateKey(t.gc, acc, output.Password) + if err != nil { + return newErrorResponse(err.Error(), errorCachePrivateKey, http.StatusInternalServerError) + } + } + return operations.NewTransferCoinOK().WithPayload( &models.OperationResponse{ OperationID: operation.OperationID, diff --git a/internal/handler/wallet/transfer_test.go b/internal/handler/wallet/transfer_test.go index 04439252c..5344cf68a 100644 --- a/internal/handler/wallet/transfer_test.go +++ b/internal/handler/wallet/transfer_test.go @@ -10,7 +10,7 @@ import ( ) func Test_transfer_handler(t *testing.T) { - api, prompterApp, _, resChan, err := MockAPI() + api, resChan, err := MockAPI() assert.NoError(t, err) handler, exist := api.HandlerFor("post", "/api/accounts/{nickname}/transfer") @@ -18,7 +18,7 @@ func Test_transfer_handler(t *testing.T) { nickname := "wallet1" password := "password" - createAccount(password, nickname, t, prompterApp) + createAccount(password, nickname, t, prompterAppMock) t.Run("Transfer with unprocessable entity", func(t *testing.T) { resp, err := handleHTTPRequest(handler, "POST", fmt.Sprintf("/api/accounts/%s/transfer", "nobody"), "") @@ -63,7 +63,7 @@ func Test_transfer_handler(t *testing.T) { // Send password to prompter app and wait for result go func(res chan walletapp.EventData) { - prompterApp.App().PromptInput <- &walletapp.SignPromptInput{ + prompterAppMock.App().PromptInput <- &walletapp.SignPromptInput{ BaseMessage: walletapp.BaseMessage{}, Password: password, Fees: "1000", diff --git a/internal/handler/wallet/update_test.go b/internal/handler/wallet/update_test.go index b8a042e16..db22aef69 100644 --- a/internal/handler/wallet/update_test.go +++ b/internal/handler/wallet/update_test.go @@ -11,14 +11,14 @@ import ( ) func Test_ModifyWallets_handler(t *testing.T) { - api, prompterApp, _, _, err := MockAPI() + api, _, err := MockAPI() assert.NoError(t, err) // Create account nickname := "trololol-old" password := "zePassword" - createAccount(password, nickname, t, prompterApp) - assert.Equal(t, prompterApp.App().Wallet.GetAccountCount(), 1, "there should be only one wallet") + createAccount(password, nickname, t, prompterAppMock) + assert.Equal(t, prompterAppMock.App().Wallet.GetAccountCount(), 1, "there should be only one wallet") handler, exist := api.HandlerFor("put", "/api/accounts/{nickname}") assert.True(t, exist, "Endpoint doesn't exist") diff --git a/pkg/app/events.go b/pkg/app/events.go index a063a3a28..5b9c02e07 100644 --- a/pkg/app/events.go +++ b/pkg/app/events.go @@ -32,6 +32,9 @@ const ( Backup TradeRolls Unprotect + AddSignRule + DeleteSignRule + UpdateSignRule ) // User input interfaces for the channel @@ -63,7 +66,11 @@ type ImportPKeyPromptInput struct { // Output interface for the caller of the prompt -type SignPromptOutput struct { +type PasswordPromptOutput struct { Password *memguard.LockedBuffer - Fees uint64 +} + +type SignPromptOutput struct { + PasswordPromptOutput + Fees uint64 } diff --git a/pkg/assets/asset.go b/pkg/assets/asset.go index 70a140027..d9fcc7851 100644 --- a/pkg/assets/asset.go +++ b/pkg/assets/asset.go @@ -55,10 +55,12 @@ type assetData struct { ChainID int64 `json:"chainID"` } +var Store *AssetsStore + // NewAssetsStore creates and initializes a new instance of AssetsStore. // If assetsJSONDir is empty, it will use the default wallet path. -func NewAssetsStore(assetsJSONDir string, massaClient *network.NodeFetcher) (*AssetsStore, error) { - store := &AssetsStore{ +func InitAssetsStore(assetsJSONDir string, massaClient *network.NodeFetcher) (*AssetsStore, error) { + Store = &AssetsStore{ Assets: make(map[string]Assets), massaClient: massaClient, } @@ -68,21 +70,21 @@ func NewAssetsStore(assetsJSONDir string, massaClient *network.NodeFetcher) (*As if err != nil { return nil, errors.Wrap(err, "Failed to get AssetsStore JSON file") } - store.assetsJSONDir = assetsJSONDir + Store.assetsJSONDir = assetsJSONDir } else { - store.assetsJSONDir = assetsJSONDir + Store.assetsJSONDir = assetsJSONDir } - if err := store.loadAccountsStore(); err != nil { + if err := Store.loadAccountsStore(); err != nil { return nil, errors.Wrap(err, "failed to create AssetsStore") } - err := store.InitDefault() + err := Store.InitDefault() if err != nil { return nil, errors.Wrap(err, "failed to create AssetsStore") } - return store, nil + return Store, nil } // loadAccountsStore loads the data from the assets JSON file into the AssetsStore. diff --git a/pkg/assets/asset_test.go b/pkg/assets/asset_test.go index e0a05fe52..81cd6127e 100644 --- a/pkg/assets/asset_test.go +++ b/pkg/assets/asset_test.go @@ -42,13 +42,13 @@ func TestLoadAccountsStore(t *testing.T) { // Create a new instance of AssetsStore and load data from the testing file nodeFetcher := network.NewNodeFetcher() - store, err := NewAssetsStore(tempDir, nodeFetcher) + _, err = InitAssetsStore(tempDir, nodeFetcher) assert.NoError(t, err) // Validate the loaded data expectedAccountName := "dummyAccount" expectedContractAddress := "0x1234567890abcdef" - asset, ok := store.Assets[expectedAccountName].ContractAssets[expectedContractAddress] + asset, ok := Store.Assets[expectedAccountName].ContractAssets[expectedContractAddress] assert.True(t, ok) assert.Equal(t, asset.Name, "DummyToken") @@ -64,20 +64,20 @@ func TestAssetExists(t *testing.T) { // Create a new instance of AssetsStore and load data from the testing file nodeFetcher := network.NewNodeFetcher() - store, err := NewAssetsStore(tempDir, nodeFetcher) + _, err = InitAssetsStore(tempDir, nodeFetcher) assert.NoError(t, err) // Test case 1: Check for an existing asset existingNickname := "dummyAccount" existingContractAddress := "0x1234567890abcdef" - exists := store.AssetExists(existingNickname, existingContractAddress) + exists := Store.AssetExists(existingNickname, existingContractAddress) assert.True(t, exists, "Expected asset to exist ") // Test case 2: Check for a non-existing asset nonExistingNickname := "nonExistingAccount" nonExistingContractAddress := "0xabcdefabcdefabcdef" - notExists := store.AssetExists(nonExistingNickname, nonExistingContractAddress) + notExists := Store.AssetExists(nonExistingNickname, nonExistingContractAddress) assert.False(t, notExists, "Expected asset to not exist ") } @@ -109,7 +109,7 @@ func TestAddAndDeleteAsset(t *testing.T) { // Create a new instance of AssetsStore and load data from the testing file nodeFetcher := network.NewNodeFetcher() - store, err := NewAssetsStore(tempDir, nodeFetcher) + _, err = InitAssetsStore(tempDir, nodeFetcher) assert.NoError(t, err) // Test case 1: Add an asset and check if it's saved to JSON @@ -126,16 +126,16 @@ func TestAddAndDeleteAsset(t *testing.T) { *assetInfo.Decimals = 18 // Add the asset - err = store.AddAsset(nickname, assetInfo) + err = Store.AddAsset(nickname, assetInfo) assert.NoError(t, err) // Check if the added asset exists - assert.True(t, store.AssetExists(nickname, assetAddress), "Added asset not found in the store") + assert.True(t, Store.AssetExists(nickname, assetAddress), "Added asset not found in the store") // Test case 2: Delete the added asset and check if it's removed from JSON - err = store.DeleteAsset(nickname, assetAddress) + err = Store.DeleteAsset(nickname, assetAddress) assert.NoError(t, err) // Check if the deleted asset no longer exists - assert.False(t, store.AssetExists(nickname, assetAddress), "Deleted asset still found in the store") + assert.False(t, Store.AssetExists(nickname, assetAddress), "Deleted asset still found in the store") } diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go new file mode 100644 index 000000000..354ade26e --- /dev/null +++ b/pkg/cache/cache.go @@ -0,0 +1,172 @@ +package cache + +import ( + "crypto/ed25519" + "fmt" + "os" + "sync" + "time" + + "github.com/awnumar/memguard" + "github.com/bluele/gcache" + "github.com/massalabs/station-massa-wallet/pkg/utils" + "github.com/massalabs/station-massa-wallet/pkg/wallet/account" + "lukechampine.com/blake3" +) + +var ( + cache gcache.Cache + once sync.Once +) + +func Get() gcache.Cache { + once.Do(func() { + cache = gcache.New(cacheSize). + LRU(). + Build() + }) + + return cache +} + +const ( + defaultExpirationTime = time.Hour * 24 * 30 + cacheKeyPrefix = "pkey" + notFoundMsg = "private key not found in cache" + cacheSize = 100 +) + +func CachePrivateKeyFromPassword(gc gcache.Cache, account *account.Account, password *memguard.LockedBuffer) error { + privateKey, err := account.PrivateKeyBytesInClear(password) + if err != nil { + return fmt.Errorf("error caching private key: %w", err) + } + + err = CachePrivateKey(gc, account, privateKey) + if err != nil { + return fmt.Errorf("error caching private key: %w", err) + } + + return nil +} + +func CachePrivateKey(gc gcache.Cache, account *account.Account, privateKey *memguard.LockedBuffer) error { + cacheKey, err := privateKeyCacheKey(account) + if err != nil { + return fmt.Errorf("%w: %w", utils.ErrPrivateKeyCache, err) + } + + key := KeyHash([]byte(cacheKey)) + + // Concatenate the key with itself to make it 64 bytes long + var extendedKey [64]byte + copy(extendedKey[:], append(key[:], key[:]...)) + + cipheredPrivateKey, err := xor(privateKey, extendedKey) + if err != nil { + return fmt.Errorf("%w: %w", utils.ErrCache, err) + } + + cacheValue := make([]byte, ed25519.PrivateKeySize) + copy(cacheValue, cipheredPrivateKey.Bytes()) + cipheredPrivateKey.Destroy() + + err = gc.SetWithExpire(key, cacheValue, expirationDuration()) + if err != nil { + return fmt.Errorf("error set private key in cache: %w", err) + } + + return nil +} + +// privateKeyFromCache return the private key from the cache or an error. +func PrivateKeyFromCache( + gc gcache.Cache, + acc *account.Account, +) (*memguard.LockedBuffer, error) { + cacheKey, err := privateKeyCacheKey(acc) + if err != nil { + return nil, fmt.Errorf("%w: %w", utils.ErrPrivateKeyCache, err) + } + + keyHash := KeyHash([]byte(cacheKey)) + + value, err := gc.Get(keyHash) + if err != nil { + if err.Error() == gcache.KeyNotFoundError.Error() { + return nil, fmt.Errorf("%w: %w", utils.ErrPrivateKeyCache, err) + } + + return nil, fmt.Errorf("%w: %w", utils.ErrCache, err) + } + + if value == nil { + return nil, fmt.Errorf("%w: %s", utils.ErrCache, notFoundMsg) + } + + byteValue, ok := value.([]byte) + if !ok { + return nil, fmt.Errorf("%w: %s", utils.ErrCache, "value is not a byte array") + } + + cacheValue := make([]byte, ed25519.PrivateKeySize) + copy(cacheValue, byteValue) + + cipheredPrivateKey := memguard.NewBufferFromBytes(cacheValue) + + // Concatenate the key with itself to make it 64 bytes long + var extendedKey [64]byte + copy(extendedKey[:], append(keyHash[:], keyHash[:]...)) + + privateKey, err := xor(cipheredPrivateKey, extendedKey) + if err != nil { + return nil, fmt.Errorf("%w: %w", utils.ErrCache, err) + } + + cipheredPrivateKey.Destroy() + + return privateKey, nil +} + +func xor(pkey *memguard.LockedBuffer, cacheKeyHash [64]byte) (*memguard.LockedBuffer, error) { + a := pkey.Bytes() + + if len(a) != len(cacheKeyHash) { + return nil, fmt.Errorf("length of two arrays must be same, %d and %d", len(a), len(cacheKeyHash)) + } + result := make([]byte, len(a)) + + for i := 0; i < len(a); i++ { + result[i] = a[i] ^ cacheKeyHash[i] + } + + return memguard.NewBufferFromBytes(result), nil +} + +func privateKeyCacheKey(account *account.Account) ([]byte, error) { + address, err := account.Address.String() + if err != nil { + return []byte(""), fmt.Errorf("err: %w", err) + } + + return []byte(cacheKeyPrefix + address), nil +} + +func KeyHash(cacheKeyHash []byte) [32]byte { + return blake3.Sum256(cacheKeyHash) +} + +func expirationDuration() time.Duration { + fromEnv := os.Getenv("PKEY_CACHE_EXPIRATION_TIME") + + if fromEnv == "" { + return defaultExpirationTime + } + + duration, err := time.ParseDuration(fromEnv) + if err != nil { + return defaultExpirationTime + } + + return duration +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 0843a0853..b3ebe8eda 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -14,73 +14,85 @@ import ( "github.com/massalabs/station-massa-wallet/pkg/wallet" ) -type SignRule struct { - Contract string `json:"contract"` - PasswordPrompt bool `json:"passwordPrompt"` - AutoSign bool `json:"autoSign"` -} - -type AccountCfg struct { - SignRules []SignRule `json:"signRules"` -} - -type Config struct { - Accounts map[string]AccountCfg `json:"accounts"` -} - var ( - cfg *Config - once sync.Once - k = koanf.New(".") + configManager *ConfigManager + once sync.Once + k = koanf.New(".") ) const ( - fileName = "wallet_config.json" + fileName = "wallet_config.json" + nanoIDLength = 10 // Length of NanoID ) -func Load() *Config { - once.Do(func() { - configDir, err := wallet.Path() - if err != nil { - log.Fatalf("Failed to get wallet config directory: %v", err) - } +var configFileDirOverride string - path := filepath.Join(configDir, fileName) +func Load() *ConfigManager { + once.Do(func() { + path := configFilePath() if _, err := os.Stat(path); os.IsNotExist(err) { - log.Printf("config file not found. Creating default config at %s", path) - createDefaultConfigFile(path, defaultConfig()) + log.Printf("Config file not found. Creating default config at %s", path) + + if err := saveConfigUnsafe(defaultConfig()); err != nil { + log.Fatalf("Error saving config: %v", err) + } } if err := k.Load(file.Provider(path), json.Parser()); err != nil { log.Fatalf("Error loading config: %v", err) } - cfg = &Config{} - if err := k.Unmarshal("", cfg); err != nil { + configManager = &ConfigManager{} + + var cfg Config + if err := k.Unmarshal("", &cfg); err != nil { log.Fatalf("Error unmarshaling config: %v", err) } + + configManager.Config = &cfg + + // Validate all rule IDs + if err := configManager.Config.validateAllRuleIDs(); err != nil { + log.Fatalf("Invalid rule IDs found: %v", err) + } + + if err := saveConfigUnsafe(configManager.Config); err != nil { + log.Fatalf("Error saving config: %v", err) + } }) - return cfg + return configManager +} + +// Used by unit test +func SetConfigFileDirOverride(path string) { + configFileDirOverride = path } func Get() *Config { - if cfg == nil { + if configManager == nil { log.Fatal("Config not loaded. Call Load() first.") } - return cfg + configManager.mu.RLock() + defer configManager.mu.RUnlock() + + return configManager.Config } func GetAccountConfig(accountName string) (*AccountCfg, error) { - cfg := Get() - - account, exists := cfg.Accounts[accountName] - if !exists { + // Use koanf's in-memory structure to get the account configuration + key := fmt.Sprintf("accounts.%s", accountName) + if !k.Exists(key) { return nil, fmt.Errorf("account '%s' not found in configuration", accountName) } + var account AccountCfg + if err := k.Unmarshal(key, &account); err != nil { + return nil, fmt.Errorf("failed to unmarshal account configuration for '%s': %v", accountName, err) + } + return &account, nil } @@ -90,17 +102,49 @@ func defaultConfig() *Config { } } -func createDefaultConfigFile(filePath string, defaultConfig *Config) { - file, err := os.Create(filePath) +func configFilePath() string { + if configFileDirOverride != "" { + return filepath.Join(configFileDirOverride, fileName) + } + + configDir, err := wallet.Path() + if err != nil { + log.Fatalf("Failed to get wallet config directory: %v", err) + } + + return filepath.Join(configDir, fileName) +} + +// Public method for saving config +func SaveConfig(config *Config) error { + configManager.mu.Lock() + defer configManager.mu.Unlock() + + return saveConfigUnsafe(config) +} + +// saveConfigUnsafe saves the config without locking. +// It should only be called from methods that already hold the lock. +func saveConfigUnsafe(config *Config) error { + // Check for duplicate IDs + if err := config.ensureUniqueRuleIDs(); err != nil { + return fmt.Errorf("duplicate SignRule IDs found: %v", err) + } + + path := configFilePath() + + file, err := os.Create(path) if err != nil { - log.Fatalf("failed to create config file: %v", err) + return fmt.Errorf("failed to create or save config file: %v", err) } defer file.Close() encoder := jsonStd.NewEncoder(file) encoder.SetIndent("", " ") // Pretty-print JSON - if err := encoder.Encode(defaultConfig); err != nil { - log.Fatalf("failed to write default config to file: %v", err) + if err := encoder.Encode(config); err != nil { + return fmt.Errorf("failed to write config to file: %v", err) } + + return nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 000000000..1e509d9f1 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,185 @@ +package config + +import ( + "fmt" + "log" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +var cfg *Config + +const accountName = "test_account" + +func TestMain(m *testing.M) { + tempDir, err := os.MkdirTemp(os.TempDir(), "*-wallet-dir") + if err != nil { + log.Fatalf("while creating temporary wallet directory: %s", err.Error()) + } + + // Load config file with config file path override + SetConfigFileDirOverride(tempDir) + manager := Load() + cfg = manager.Config + + os.Exit(m.Run()) +} + +func TestAddSignRule(t *testing.T) { + rule := SignRule{ + Name: "Test Rule", + Contract: "test_contract", + RuleType: RuleTypeAutoSign, + Enabled: true, + } + + ruleID, err := cfg.AddSignRule(accountName, rule) + assert.NoError(t, err) + assert.NotEmpty(t, ruleID) + + // Verify the rule was added + account, exists := cfg.Accounts[accountName] + assert.True(t, exists) + assert.Equal(t, 1, len(account.SignRules)) + assert.Equal(t, ruleID, account.SignRules[0].ID) + + // Add another rule + rule2 := SignRule{ + Name: "Test Rule 2", + Contract: "test_contract_2", + RuleType: RuleTypeDisablePasswordPrompt, + Enabled: false, + } + + ruleID, err = cfg.AddSignRule(accountName, rule2) + assert.NoError(t, err) + assert.NotEmpty(t, ruleID) + + // Verify the rule was added + account, exists = cfg.Accounts[accountName] + assert.True(t, exists) + assert.Equal(t, 2, len(account.SignRules)) + assert.Equal(t, ruleID, account.SignRules[1].ID) + + // Add the same rule again and verify it fails (duplicate rule) + _, err = cfg.AddSignRule(accountName, rule) + assert.Error(t, err) + + _, err = cfg.AddSignRule(accountName, rule2) + assert.Error(t, err) + + // check there is no duplicate rule + err = cfg.ensureUniqueRuleIDs() + assert.NoError(t, err) +} + +func TestDeleteSignRule(t *testing.T) { + contract := "test_delete_contract" + + rule := SignRule{ + Name: "Test Rule", + Contract: contract, + RuleType: RuleTypeAutoSign, + Enabled: true, + } + + fmt.Println("TestDeleteSignRule: adding rule", rule) + + ruleID, err := cfg.AddSignRule(accountName, rule) + assert.NoError(t, err) + + err = cfg.DeleteSignRule(accountName, ruleID) + assert.NoError(t, err) + + // Verify the rule was deleted + deletedRule, err := cfg.GetSignRule(accountName, ruleID) + assert.Error(t, err, "rule not found") + assert.Nil(t, deletedRule) +} + +func TestUpdateSignRule(t *testing.T) { + accountName := "test_account" + contract := "test_update_contract" + rule := SignRule{ + Name: "Test Rule", + Contract: contract, + RuleType: RuleTypeAutoSign, + Enabled: true, + } + + ruleID, err := cfg.AddSignRule(accountName, rule) + assert.NoError(t, err) + + newRule := SignRule{ + Name: "Updated Rule", + Contract: "updated_contract", + RuleType: RuleTypeDisablePasswordPrompt, + Enabled: false, + } + + newRuleID, err := cfg.UpdateSignRule(accountName, ruleID, newRule) + assert.NoError(t, err) + assert.NotEqual(t, ruleID, newRuleID) + + // Verify the rule was updated + updatedRule, err := cfg.GetSignRule(accountName, newRuleID) + assert.NoError(t, err) + assert.Equal(t, newRuleID, updatedRule.ID) + assert.Equal(t, newRule.Name, updatedRule.Name) + assert.Equal(t, newRule.Contract, updatedRule.Contract) + assert.Equal(t, newRule.RuleType, updatedRule.RuleType) + assert.Equal(t, newRule.Enabled, updatedRule.Enabled) + + // Verify previous rule is deleted + deletedRule, err := cfg.GetSignRule(accountName, ruleID) + assert.Error(t, err, "rule not found") + assert.Nil(t, deletedRule) +} + +func TestValidateRuleID(t *testing.T) { + rule := SignRule{ + Name: "Test Rule", + Contract: "test_contract", + RuleType: RuleTypeAutoSign, + Enabled: true, + } + + rule.ID = generateRuleID(accountName, rule) + err := ValidateRuleID(accountName, rule) + assert.NoError(t, err) + + // Test with an invalid ID + rule.ID = "invalid_id" + err = ValidateRuleID(accountName, rule) + assert.Error(t, err) +} + +func TestHasEnabledRule(t *testing.T) { + accountName := "test_account" + contract := "test_hasEnable_contract" + rule := SignRule{ + Name: "Test Rule", + Contract: contract, + RuleType: RuleTypeAutoSign, + Enabled: true, + } + + ruleId, err := cfg.AddSignRule(accountName, rule) + assert.NoError(t, err) + + hasEnabled := cfg.HasEnabledRule(accountName) + assert.True(t, hasEnabled) + + rulePtr := cfg.GetEnabledRuleForContract(accountName, &contract) + assert.NotNil(t, rulePtr) + + // Disable the rule and check again + rule.Enabled = false + _, err = cfg.UpdateSignRule(accountName, ruleId, rule) + assert.NoError(t, err) + + rulePtr = cfg.GetEnabledRuleForContract(accountName, &contract) + assert.Nil(t, rulePtr) +} diff --git a/pkg/config/rules.go b/pkg/config/rules.go new file mode 100644 index 000000000..105c69f30 --- /dev/null +++ b/pkg/config/rules.go @@ -0,0 +1,261 @@ +package config + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" +) + +func (c *Config) AddSignRule(accountName string, rule SignRule) (string, error) { + if err := validateRule(rule); err != nil { + return "", fmt.Errorf("invalid rule: %v", err) + } + + rule.ID = generateRuleID(accountName, rule) + + configManager.mu.Lock() + defer configManager.mu.Unlock() + + // Check if the account exists + account, exists := c.Accounts[accountName] + if !exists { + account = AccountCfg{SignRules: []SignRule{}} + c.Accounts[accountName] = account + } + + // check if the rule already exists + _, err := c.getSignRuleUnsafe(accountName, rule.ID) + if err == nil { + return "", fmt.Errorf("rule %s already exists", rule.ID) + } + + // Add the new rule + account.SignRules = append(account.SignRules, rule) + c.Accounts[accountName] = account + + err = saveConfigUnsafe(c) + if err != nil { + return "", fmt.Errorf("error saving configuration: %v", err) + } + + return rule.ID, nil +} + +func (c *Config) DeleteSignRule(accountName, ruleID string) error { + configManager.mu.Lock() + defer configManager.mu.Unlock() + + // Check if the account exists + account, exists := c.Accounts[accountName] + if !exists { + return fmt.Errorf("account not found: %s", accountName) + } + + // Find the index of the rule to delete + index := -1 + + for i, rule := range account.SignRules { + if rule.ID == ruleID { + index = i + break + } + } + + if index == -1 { + return fmt.Errorf("rule not found: %s", ruleID) + } + + // Remove the rule from the slice + account.SignRules = append(account.SignRules[:index], account.SignRules[index+1:]...) + c.Accounts[accountName] = account + + err := saveConfigUnsafe(c) + if err != nil { + return fmt.Errorf("error saving configuration: %v", err) + } + + return nil +} + +func (c *Config) UpdateSignRule(accountName, ruleID string, newRule SignRule) (string, error) { + configManager.mu.Lock() + defer configManager.mu.Unlock() + + // Check if the account exists + account, exists := c.Accounts[accountName] + if !exists { + return "", fmt.Errorf("account not found: %s", accountName) + } + + // Find the index of the rule to update + index := -1 + + for i, rule := range account.SignRules { + if rule.ID == ruleID { + index = i + break + } + } + + if index == -1 { + return "", fmt.Errorf("rule not found: %s", ruleID) + } + + // Delete the existing rule + account.SignRules = append(account.SignRules[:index], account.SignRules[index+1:]...) + c.Accounts[accountName] = account + + // Add the new rule + newRule.ID = generateRuleID(accountName, newRule) + account.SignRules = append(account.SignRules, newRule) + c.Accounts[accountName] = account + + err := saveConfigUnsafe(c) + if err != nil { + return "", fmt.Errorf("error saving configuration: %v", err) + } + + return newRule.ID, nil +} + +func validateRule(rule SignRule) error { + switch rule.RuleType { + case RuleTypeDisablePasswordPrompt: + return nil + + case RuleTypeAutoSign: + if rule.Contract == "*" { + return fmt.Errorf("RuleTypeAutoSign cannot have a wildcard contract") + } + + return nil + + default: + return fmt.Errorf("invalid ruleType: %s", rule.RuleType) + } +} + +// Two rules with the same SignRule will always generate the same ID. +func generateRuleID(accountName string, rule SignRule) string { + // Create a string that combines all rule parameters + ruleString := fmt.Sprintf("%s:%s:%s", accountName, rule.Contract, string(rule.RuleType)) + + hash := sha256.Sum256([]byte(ruleString)) + + // Convert the first 10 bytes of the hash to a hex string + return hex.EncodeToString(hash[:5]) +} + +func (c *Config) ensureUniqueRuleIDs() error { + idMap := make(map[string]bool) + + for _, account := range c.Accounts { + for _, rule := range account.SignRules { + if idMap[rule.ID] { + return fmt.Errorf("duplicate ID found: %s", rule.ID) + } + + idMap[rule.ID] = true + } + } + + return nil +} + +func ValidateRuleID(accountName string, rule SignRule) error { + // Generate the expected ID based on the rule's parameters + expectedID := generateRuleID(accountName, rule) + + // Compare the expected ID with the actual ID + if rule.ID != expectedID { + return fmt.Errorf("rule ID mismatch: expected %s, got %s", expectedID, rule.ID) + } + + return nil +} + +func (c *Config) validateAllRuleIDs() error { + for accountName, account := range c.Accounts { + for i, rule := range account.SignRules { + if err := ValidateRuleID(accountName, rule); err != nil { + return fmt.Errorf("invalid rule ID in account '%s', rule index %d: %v", accountName, i, err) + } + } + } + + return nil +} + +func (c *Config) getSignRuleUnsafe(accountName, ruleID string) (*SignRule, error) { + // Check if the account exists + account, exists := c.Accounts[accountName] + if !exists { + return nil, fmt.Errorf("account not found: %s", accountName) + } + + // Search for the rule by ID + for _, rule := range account.SignRules { + if rule.ID == ruleID { + return &rule, nil + } + } + + return nil, fmt.Errorf("rule not found: %s", ruleID) +} + +func (c *Config) GetSignRule(accountName, ruleID string) (*SignRule, error) { + configManager.mu.RLock() + defer configManager.mu.RUnlock() + + return c.getSignRuleUnsafe(accountName, ruleID) +} + +func (c *Config) HasEnabledRule(accountName string) bool { + configManager.mu.RLock() + defer configManager.mu.RUnlock() + + // Check if the account exists + account, exists := c.Accounts[accountName] + if !exists { + return false + } + + // Check if there is any enabled rule + for _, rule := range account.SignRules { + if rule.Enabled { + return true + } + } + + return false +} + +func (c *Config) GetEnabledRuleForContract(accountName string, contract *string) *RuleType { + configManager.mu.RLock() + defer configManager.mu.RUnlock() + + // Check if the account exists + account, exists := c.Accounts[accountName] + if !exists { + return nil + } + + // Check if there is any enabled rule that applies to the contract + for _, rule := range account.SignRules { + if rule.Enabled { + switch rule.RuleType { + case RuleTypeAutoSign: + if contract != nil && rule.Contract == *contract { + return &rule.RuleType + } + + case RuleTypeDisablePasswordPrompt: + if rule.Contract == "*" || (contract != nil && rule.Contract == *contract) { + return &rule.RuleType + } + } + } + } + + return nil +} diff --git a/pkg/config/types.go b/pkg/config/types.go new file mode 100644 index 000000000..c8068f9fd --- /dev/null +++ b/pkg/config/types.go @@ -0,0 +1,30 @@ +package config + +import "sync" + +type RuleType string + +const ( + RuleTypeDisablePasswordPrompt RuleType = "disable_password_prompt" + RuleTypeAutoSign RuleType = "auto_sign" +) + +type SignRule struct { + Name string `koanf:"name"` + ID string `koanf:"id"` + Contract string `koanf:"contract"` + RuleType RuleType `koanf:"ruleType"` + Enabled bool `koanf:"enabled"` +} + +type AccountCfg struct { + SignRules []SignRule `koanf:"signRules"` +} + +type Config struct { + Accounts map[string]AccountCfg `koanf:"accounts"` +} +type ConfigManager struct { + mu sync.RWMutex + Config *Config +} diff --git a/pkg/prompt/prompt.go b/pkg/prompt/prompt.go index 5a1f0674a..ed7417181 100644 --- a/pkg/prompt/prompt.go +++ b/pkg/prompt/prompt.go @@ -13,10 +13,11 @@ import ( ) type PromptRequest struct { - Action walletapp.PromptRequestAction - Msg string - Data interface{} - CodeMessage string + Action walletapp.PromptRequestAction + Msg string + Data interface{} + CodeMessage string + PasswordRequired bool } // WalletPrompter is a struct that wraps a Wails GUI application and implements the WalletPrompterInterface interface. @@ -79,10 +80,10 @@ func WakeUpPrompt( var err error switch req.Action { - case walletapp.Delete, walletapp.Unprotect: + case walletapp.Delete, walletapp.Unprotect, walletapp.AddSignRule, walletapp.UpdateSignRule, walletapp.DeleteSignRule: output, keepListening, err = handlePasswordPrompt(prompterApp, input, acc) case walletapp.Sign: - output, keepListening, err = handleSignPrompt(prompterApp, input, acc) + output, keepListening, err = handleSignPrompt(prompterApp, req, input, acc) case walletapp.NewPassword: output, keepListening, err = handleNewPasswordPrompt(prompterApp, input) case walletapp.Import: diff --git a/pkg/prompt/sign.go b/pkg/prompt/sign.go index 4a172c40f..69643aad8 100644 --- a/pkg/prompt/sign.go +++ b/pkg/prompt/sign.go @@ -5,13 +5,40 @@ import ( "strconv" "github.com/awnumar/memguard" + "github.com/massalabs/station-massa-wallet/api/server/models" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" + "github.com/massalabs/station-massa-wallet/pkg/config" "github.com/massalabs/station-massa-wallet/pkg/utils" "github.com/massalabs/station-massa-wallet/pkg/wallet/account" ) +type PromptRequestSignData struct { + Description string + Fees string + MinFees string + OperationType int + Coins string + Address string + Function string + MaxCoins string + WalletAddress string + Nickname string + RollCount uint64 + RecipientAddress string + RecipientNickname string + Amount string + PlainText string + AllowFeeEdition bool + ChainID int64 + Assets []models.AssetInfo + Parameters []byte + DeployedByteCodeSize uint // for executeSC of type deploySC + DeployedCoins uint64 // for executeSC of type DeploySC; the number of coins sent to the deployed contract + EnabledSignRule *config.RuleType +} + // handleSignPrompt returns the password as a LockedBuffer, or an error if the input is not a string. -func handleSignPrompt(prompterApp WalletPrompterInterface, input interface{}, acc *account.Account) (*walletapp.SignPromptOutput, bool, error) { +func handleSignPrompt(prompterApp WalletPrompterInterface, req PromptRequest, input interface{}, acc *account.Account) (*walletapp.SignPromptOutput, bool, error) { inputObject, ok := input.(*walletapp.SignPromptInput) if !ok { return nil, true, InputTypeError(prompterApp) @@ -25,6 +52,15 @@ func handleSignPrompt(prompterApp WalletPrompterInterface, input interface{}, ac return nil, true, fmt.Errorf("failed to parse fees: %w", err) } + data, ok := req.Data.(PromptRequestSignData) + + if ok && data.EnabledSignRule != nil && !req.PasswordRequired { + // if sign rule is enabled, we don't need to check password + return &walletapp.SignPromptOutput{ + Fees: fees, + }, false, nil + } + inputString := inputObject.Password password := memguard.NewBufferFromBytes([]byte(inputString)) @@ -42,8 +78,10 @@ func handleSignPrompt(prompterApp WalletPrompterInterface, input interface{}, ac } output := &walletapp.SignPromptOutput{ - Password: passwordReturned, - Fees: fees, + PasswordPromptOutput: walletapp.PasswordPromptOutput{ + Password: passwordReturned, + }, + Fees: fees, } return output, false, nil diff --git a/pkg/types/address.go b/pkg/types/address.go index 1f1f18f8f..6ddd49e28 100644 --- a/pkg/types/address.go +++ b/pkg/types/address.go @@ -1,6 +1,8 @@ package types import ( + "fmt" + "github.com/massalabs/station-massa-wallet/pkg/types/object" "lukechampine.com/blake3" ) @@ -56,6 +58,16 @@ func (a *Address) MarshalText() ([]byte, error) { return a.Object.MarshalText() } +// String returns the Address as a string by marshaling it to text. +func (a *Address) String() (string, error) { + data, err := a.MarshalText() + if err != nil { + return "", fmt.Errorf("failed to marshal address: %w", err) + } + + return string(data), nil +} + // UnmarshalText overloads the TextUnmarshaler interface for Address. func (a *Address) UnmarshalText(text []byte) error { if a.Object == nil { diff --git a/pkg/utils/errors.go b/pkg/utils/errors.go index 1518ef06f..0301dc227 100644 --- a/pkg/utils/errors.go +++ b/pkg/utils/errors.go @@ -34,12 +34,12 @@ const ( // Sentinel errors var ( - ErrPrivateKeyCache = errors.New("Private key not found in cache") - ErrCache = errors.New("Error loading cache") + ErrPrivateKeyCache = errors.New("private key not found in cache") + ErrCache = errors.New("error loading cache") ErrWrongPassword = errors.New("wrong password") - ErrActionCanceled = errors.New("Action canceled by user") + ErrActionCanceled = errors.New("action canceled by user") ErrInvalidInputType = errors.New("invalid prompt input type") - ErrTimeout = errors.New("Password prompt reached timeout") + ErrTimeout = errors.New("password prompt reached timeout") ) func WailsErrorCode(err error) string { diff --git a/web-frontend/src/i18n/en_US.json b/web-frontend/src/i18n/en_US.json index e62f11c25..5092157ac 100644 --- a/web-frontend/src/i18n/en_US.json +++ b/web-frontend/src/i18n/en_US.json @@ -44,7 +44,7 @@ "placeholder": "Token Address", "import-button": "Import token", "import-title": "Import a Fungible Token", - "import-subtitle": "Input the FT1 token’s address", + "import-subtitle": "Input the MRC20 token’s address", "success": "Token successfully added", "success-screen": { "success-message": "{name} ({symbol}) token is added to your wallet.", From c8e8cf44f2983bba903b304417cdc50c0b901b09 Mon Sep 17 00:00:00 2001 From: Peterjah Date: Tue, 21 Jan 2025 10:47:12 +0100 Subject: [PATCH 06/10] Use singleton for cache and datastore --- internal/handler/wallet/add_asset.go | 8 +- internal/handler/wallet/api.go | 22 ++- internal/handler/wallet/cache.go | 155 -------------------- internal/handler/wallet/delete_asset.go | 14 +- internal/handler/wallet/get_all_assets.go | 6 +- internal/handler/wallet/sign.go | 13 +- internal/handler/wallet/sign_message.go | 9 +- internal/handler/wallet/sign_rule_add.go | 9 +- internal/handler/wallet/sign_rule_delete.go | 6 +- internal/handler/wallet/sign_rule_test.go | 3 +- internal/handler/wallet/sign_rule_update.go | 9 +- internal/handler/wallet/sign_test.go | 14 +- internal/handler/wallet/transfer.go | 9 +- pkg/cache/cache.go | 11 +- 14 files changed, 55 insertions(+), 233 deletions(-) delete mode 100644 internal/handler/wallet/cache.go diff --git a/internal/handler/wallet/add_asset.go b/internal/handler/wallet/add_asset.go index 948c66c3e..3379a88c5 100644 --- a/internal/handler/wallet/add_asset.go +++ b/internal/handler/wallet/add_asset.go @@ -13,15 +13,13 @@ type AssetInfoListResponse struct { Assets []models.AssetInfo `json:"assets"` } -func NewAddAsset(AssetsStore *assets.AssetsStore, massaClient network.NodeFetcherInterface) operations.AddAssetHandler { +func NewAddAsset(massaClient network.NodeFetcherInterface) operations.AddAssetHandler { return &addAsset{ - AssetsStore: AssetsStore, massaClient: massaClient, } } type addAsset struct { - AssetsStore *assets.AssetsStore massaClient network.NodeFetcherInterface } @@ -41,7 +39,7 @@ func (a *addAsset) Handle(params operations.AddAssetParams) middleware.Responder } // Check if the address exists in the loaded JSON - if a.AssetsStore.AssetExists(params.Nickname, params.AssetAddress) { + if assets.Store.AssetExists(params.Nickname, params.AssetAddress) { // Return that the asset already exists errorMsg := "Asset with the provided address already exists." return operations.NewAddAssetBadRequest().WithPayload(&models.Error{Code: errorAssetExists, Message: errorMsg}) @@ -56,7 +54,7 @@ func (a *addAsset) Handle(params operations.AddAssetParams) middleware.Responder } // Add Asset and persist in JSON file. - if err := a.AssetsStore.AddAsset(params.Nickname, *assetInfoFromSC); err != nil { + if err := assets.Store.AddAsset(params.Nickname, *assetInfoFromSC); err != nil { // Return error occurred while persisting the asset errorMsg := "Failed to add the asset to the JSON file." return operations.NewAddAssetInternalServerError().WithPayload(&models.Error{Code: errorAddAssetJSON, Message: errorMsg}) diff --git a/internal/handler/wallet/api.go b/internal/handler/wallet/api.go index d73d7ff6e..87e4fe9e3 100644 --- a/internal/handler/wallet/api.go +++ b/internal/handler/wallet/api.go @@ -8,8 +8,6 @@ import ( "github.com/go-openapi/runtime/middleware" "github.com/massalabs/station-massa-wallet/api/server/models" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" - "github.com/massalabs/station-massa-wallet/pkg/assets" - "github.com/massalabs/station-massa-wallet/pkg/cache" "github.com/massalabs/station-massa-wallet/pkg/network" "github.com/massalabs/station-massa-wallet/pkg/openapi" "github.com/massalabs/station-massa-wallet/pkg/prompt" @@ -21,27 +19,25 @@ import ( // AppendEndpoints appends wallet endpoints to the API // Note: the password prompter is mandatory for sign endpoint func AppendEndpoints(api *operations.MassaWalletAPI, prompterApp prompt.WalletPrompterInterface, massaClient network.NodeFetcherInterface) { - gc := cache.Get() - assetStore := assets.Store api.CreateAccountHandler = NewCreateAccount(prompterApp, massaClient) api.DeleteAccountHandler = NewDelete(prompterApp, massaClient) api.ImportAccountHandler = NewImport(prompterApp, massaClient) api.AccountListHandler = NewGetAll(prompterApp.App().Wallet, massaClient) - api.SignHandler = NewSign(prompterApp, gc, assetStore) - api.SignMessageHandler = NewSignMessage(prompterApp, gc) + api.SignHandler = NewSign(prompterApp) + api.SignMessageHandler = NewSignMessage(prompterApp) api.GetAccountHandler = NewGet(prompterApp, massaClient) api.ExportAccountFileHandler = NewWalletExportFile(prompterApp.App().Wallet) - api.TransferCoinHandler = NewTransferCoin(prompterApp, massaClient, gc) + api.TransferCoinHandler = NewTransferCoin(prompterApp, massaClient) api.TradeRollsHandler = NewTradeRolls(prompterApp, massaClient) api.BackupAccountHandler = NewBackupAccount(prompterApp) api.UpdateAccountHandler = NewUpdateAccount(prompterApp, massaClient) - api.AddAssetHandler = NewAddAsset(assetStore, massaClient) - api.GetAllAssetsHandler = NewGetAllAssets(prompterApp.App().Wallet, assetStore, massaClient) - api.DeleteAssetHandler = NewDeleteAsset(assetStore) + api.AddAssetHandler = NewAddAsset(massaClient) + api.GetAllAssetsHandler = NewGetAllAssets(prompterApp.App().Wallet, massaClient) + api.DeleteAssetHandler = NewDeleteAsset() api.GetConfigHandler = NewGetConfig() - api.AddSignRuleHandler = NewAddSignRuleHandler(prompterApp, gc) - api.DeleteSignRuleHandler = NewDeleteSignRuleHandler(prompterApp, gc) - api.UpdateSignRuleHandler = NewUpdateSignRuleHandler(prompterApp, gc) + api.AddSignRuleHandler = NewAddSignRuleHandler(prompterApp) + api.DeleteSignRuleHandler = NewDeleteSignRuleHandler(prompterApp) + api.UpdateSignRuleHandler = NewUpdateSignRuleHandler(prompterApp) } // loadAccount loads a wallet from the file system or returns an error. diff --git a/internal/handler/wallet/cache.go b/internal/handler/wallet/cache.go deleted file mode 100644 index a19c900d9..000000000 --- a/internal/handler/wallet/cache.go +++ /dev/null @@ -1,155 +0,0 @@ -package wallet - -import ( - "crypto/ed25519" - "fmt" - "os" - "time" - - "github.com/awnumar/memguard" - "github.com/bluele/gcache" - "github.com/massalabs/station-massa-wallet/pkg/utils" - "github.com/massalabs/station-massa-wallet/pkg/wallet/account" - "lukechampine.com/blake3" -) - -const ( - defaultExpirationTime = time.Hour * 24 * 30 - cacheKeyPrefix = "pkey" - notFoundMsg = "private key not found in cache" -) - -func CachePrivateKeyFromPassword(gc gcache.Cache, account *account.Account, password *memguard.LockedBuffer) error { - privateKey, err := account.PrivateKeyBytesInClear(password) - if err != nil { - return fmt.Errorf("error caching private key: %w", err) - } - - err = CachePrivateKey(gc, account, privateKey) - if err != nil { - return fmt.Errorf("error caching private key: %w", err) - } - - return nil -} - -func CachePrivateKey(gc gcache.Cache, account *account.Account, privateKey *memguard.LockedBuffer) error { - cacheKey, err := privateKeyCacheKey(account) - if err != nil { - return fmt.Errorf("%w: %w", utils.ErrPrivateKeyCache, err) - } - - key := KeyHash([]byte(cacheKey)) - - // Concatenate the key with itself to make it 64 bytes long - var extendedKey [64]byte - copy(extendedKey[:], append(key[:], key[:]...)) - - cipheredPrivateKey, err := Xor(privateKey, extendedKey) - if err != nil { - return fmt.Errorf("%w: %w", utils.ErrCache, err) - } - - cacheValue := make([]byte, ed25519.PrivateKeySize) - copy(cacheValue, cipheredPrivateKey.Bytes()) - cipheredPrivateKey.Destroy() - - err = gc.SetWithExpire(key, cacheValue, expirationDuration()) - if err != nil { - return fmt.Errorf("error set private key in cache: %w", err) - } - - return nil -} - -// privateKeyFromCache return the private key from the cache or an error. -func privateKeyFromCache( - gc gcache.Cache, - acc *account.Account, -) (*memguard.LockedBuffer, error) { - cacheKey, err := privateKeyCacheKey(acc) - if err != nil { - return nil, fmt.Errorf("%w: %w", utils.ErrPrivateKeyCache, err) - } - - keyHash := KeyHash([]byte(cacheKey)) - - value, err := gc.Get(keyHash) - if err != nil { - if err.Error() == gcache.KeyNotFoundError.Error() { - return nil, fmt.Errorf("%w: %w", utils.ErrPrivateKeyCache, err) - } - - return nil, fmt.Errorf("%w: %w", utils.ErrCache, err) - } - - if value == nil { - return nil, fmt.Errorf("%w: %s", utils.ErrCache, notFoundMsg) - } - - byteValue, ok := value.([]byte) - if !ok { - return nil, fmt.Errorf("%w: %s", utils.ErrCache, "value is not a byte array") - } - - cacheValue := make([]byte, ed25519.PrivateKeySize) - copy(cacheValue, byteValue) - - cipheredPrivateKey := memguard.NewBufferFromBytes(cacheValue) - - // Concatenate the key with itself to make it 64 bytes long - var extendedKey [64]byte - copy(extendedKey[:], append(keyHash[:], keyHash[:]...)) - - privateKey, err := Xor(cipheredPrivateKey, extendedKey) - if err != nil { - return nil, fmt.Errorf("%w: %w", utils.ErrCache, err) - } - - cipheredPrivateKey.Destroy() - - return privateKey, nil -} - -func Xor(pkey *memguard.LockedBuffer, cacheKeyHash [64]byte) (*memguard.LockedBuffer, error) { - a := pkey.Bytes() - - if len(a) != len(cacheKeyHash) { - return nil, fmt.Errorf("length of two arrays must be same, %d and %d", len(a), len(cacheKeyHash)) - } - result := make([]byte, len(a)) - - for i := 0; i < len(a); i++ { - result[i] = a[i] ^ cacheKeyHash[i] - } - - return memguard.NewBufferFromBytes(result), nil -} - -func privateKeyCacheKey(account *account.Account) ([]byte, error) { - address, err := account.Address.String() - if err != nil { - return []byte(""), fmt.Errorf("err: %w", err) - } - - return []byte(cacheKeyPrefix + address), nil -} - -func KeyHash(cacheKeyHash []byte) [32]byte { - return blake3.Sum256(cacheKeyHash) -} - -func expirationDuration() time.Duration { - fromEnv := os.Getenv("PKEY_CACHE_EXPIRATION_TIME") - - if fromEnv == "" { - return defaultExpirationTime - } - - duration, err := time.ParseDuration(fromEnv) - if err != nil { - return defaultExpirationTime - } - - return duration -} diff --git a/internal/handler/wallet/delete_asset.go b/internal/handler/wallet/delete_asset.go index f31b642bb..c9fce6f57 100644 --- a/internal/handler/wallet/delete_asset.go +++ b/internal/handler/wallet/delete_asset.go @@ -8,15 +8,11 @@ import ( "github.com/massalabs/station-massa-wallet/pkg/utils" ) -func NewDeleteAsset(AssetsStore *assets.AssetsStore) operations.DeleteAssetHandler { - return &deleteAsset{ - AssetsStore: AssetsStore, - } +func NewDeleteAsset() operations.DeleteAssetHandler { + return &deleteAsset{} } -type deleteAsset struct { - AssetsStore *assets.AssetsStore -} +type deleteAsset struct{} func (d *deleteAsset) Handle(params operations.DeleteAssetParams) middleware.Responder { // Check if the address is valid @@ -27,14 +23,14 @@ func (d *deleteAsset) Handle(params operations.DeleteAssetParams) middleware.Res } // Check if the asset exists in the loaded JSON - if !d.AssetsStore.AssetExists(params.Nickname, params.AssetAddress) { + if !assets.Store.AssetExists(params.Nickname, params.AssetAddress) { // Return an error indicating that the asset does not exist errorMsg := "Asset with the provided address does not exist." return operations.NewDeleteAssetBadRequest().WithPayload(&models.Error{Code: errorAssetNotExists, Message: errorMsg}) } // Delete Asset From the JSON file. - if err := d.AssetsStore.DeleteAsset(params.Nickname, params.AssetAddress); err != nil { + if err := assets.Store.DeleteAsset(params.Nickname, params.AssetAddress); err != nil { // Return error occurred while persisting the asset errorMsg := "Failed to delete the asset from the JSON file." return operations.NewDeleteAssetInternalServerError().WithPayload(&models.Error{Code: errorDeleteAssetJSON, Message: errorMsg}) diff --git a/internal/handler/wallet/get_all_assets.go b/internal/handler/wallet/get_all_assets.go index ab6851794..588dcb87a 100644 --- a/internal/handler/wallet/get_all_assets.go +++ b/internal/handler/wallet/get_all_assets.go @@ -15,17 +15,15 @@ import ( "github.com/massalabs/station/pkg/logger" ) -func NewGetAllAssets(wallet *wallet.Wallet, AssetsStore *assets.AssetsStore, massaClient network.NodeFetcherInterface) operations.GetAllAssetsHandler { +func NewGetAllAssets(wallet *wallet.Wallet, massaClient network.NodeFetcherInterface) operations.GetAllAssetsHandler { return &getAllAssets{ wallet: wallet, - AssetsStore: AssetsStore, massaClient: massaClient, } } type getAllAssets struct { wallet *wallet.Wallet - AssetsStore *assets.AssetsStore massaClient network.NodeFetcherInterface } @@ -124,7 +122,7 @@ func (g *getAllAssets) getMASAsset(acc *account.Account) (*assets.AssetInfoWithB // getAssetsData fetches the balance and dollar value for each asset in the account. // If user has asset that are deployed on another network, it will not be included. func (g *getAllAssets) getAssetsData(acc *account.Account, chainID int) []*assets.AssetInfoWithBalances { - assetsInfo := g.AssetsStore.All(acc.Nickname, chainID) + assetsInfo := assets.Store.All(acc.Nickname, chainID) assetsWithBalance := make([]*assets.AssetInfoWithBalances, 0) var wg sync.WaitGroup diff --git a/internal/handler/wallet/sign.go b/internal/handler/wallet/sign.go index 88195a2a3..460d40e65 100644 --- a/internal/handler/wallet/sign.go +++ b/internal/handler/wallet/sign.go @@ -7,7 +7,6 @@ import ( "strconv" "github.com/awnumar/memguard" - "github.com/bluele/gcache" "github.com/go-openapi/runtime/middleware" "github.com/massalabs/station-massa-wallet/api/server/models" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" @@ -34,14 +33,12 @@ const ( RollPrice = 100 ) -func NewSign(prompterApp prompt.WalletPrompterInterface, gc gcache.Cache, AssetsStore *assets.AssetsStore) operations.SignHandler { - return &walletSign{gc: gc, prompterApp: prompterApp, AssetsStore: AssetsStore} +func NewSign(prompterApp prompt.WalletPrompterInterface) operations.SignHandler { + return &walletSign{prompterApp: prompterApp} } type walletSign struct { prompterApp prompt.WalletPrompterInterface - gc gcache.Cache - AssetsStore *assets.AssetsStore } func (w *walletSign) Handle(params operations.SignParams) middleware.Responder { @@ -70,7 +67,7 @@ func (w *walletSign) Handle(params operations.SignParams) middleware.Responder { if enabledRule != nil { // at this point, we have a rule enabled for the contract, if private key is cached, we don't need to prompt for password - privateKey, err = cache.PrivateKeyFromCache(w.gc, acc) + privateKey, err = cache.PrivateKeyFromCache(acc) if err != nil { logger.Warn("error retriving private key from cache: ", err) } else if privateKey != nil { @@ -108,7 +105,7 @@ func (w *walletSign) Handle(params operations.SignParams) middleware.Responder { } if cfg.HasEnabledRule(acc.Nickname) { - err = cache.CachePrivateKey(w.gc, acc, privateKey) + err = cache.CachePrivateKey(acc, privateKey) if err != nil { return newErrorResponse(err.Error(), errorCachePrivateKey, http.StatusInternalServerError) } @@ -225,7 +222,7 @@ func (w *walletSign) getPromptRequest(params operations.SignParams, acc *account data.OperationType = int(opType) data.AllowFeeEdition = *params.AllowFeeEdition data.ChainID = *params.Body.ChainID - data.Assets = convertAssetsToModel(w.AssetsStore.All(acc.Nickname, int(data.ChainID))) + data.Assets = convertAssetsToModel(assets.Store.All(acc.Nickname, int(data.ChainID))) promptRequest := prompt.PromptRequest{ Action: walletapp.Sign, diff --git a/internal/handler/wallet/sign_message.go b/internal/handler/wallet/sign_message.go index a357213cc..91f3e58ce 100644 --- a/internal/handler/wallet/sign_message.go +++ b/internal/handler/wallet/sign_message.go @@ -5,11 +5,11 @@ import ( "fmt" "net/http" - "github.com/bluele/gcache" "github.com/go-openapi/runtime/middleware" "github.com/massalabs/station-massa-wallet/api/server/models" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" + "github.com/massalabs/station-massa-wallet/pkg/cache" "github.com/massalabs/station-massa-wallet/pkg/config" "github.com/massalabs/station-massa-wallet/pkg/prompt" "github.com/massalabs/station-massa-wallet/pkg/utils" @@ -29,13 +29,12 @@ type PromptRequestSignMessageData struct { DisplayData bool } -func NewSignMessage(prompterApp prompt.WalletPrompterInterface, gc gcache.Cache) operations.SignMessageHandler { - return &walletSignMessage{gc: gc, prompterApp: prompterApp} +func NewSignMessage(prompterApp prompt.WalletPrompterInterface) operations.SignMessageHandler { + return &walletSignMessage{prompterApp: prompterApp} } type walletSignMessage struct { prompterApp prompt.WalletPrompterInterface - gc gcache.Cache } func (w *walletSignMessage) Handle(params operations.SignMessageParams) middleware.Responder { @@ -73,7 +72,7 @@ func (w *walletSignMessage) Handle(params operations.SignMessageParams) middlewa cfg := config.Get() if cfg.HasEnabledRule(acc.Nickname) { - err = CachePrivateKey(w.gc, acc, output.Password) + err = cache.CachePrivateKey(acc, output.Password) if err != nil { return newErrorResponse(err.Error(), errorCachePrivateKey, http.StatusInternalServerError) } diff --git a/internal/handler/wallet/sign_rule_add.go b/internal/handler/wallet/sign_rule_add.go index 9938b0637..9d4436760 100644 --- a/internal/handler/wallet/sign_rule_add.go +++ b/internal/handler/wallet/sign_rule_add.go @@ -4,11 +4,11 @@ import ( "fmt" "net/http" - "github.com/bluele/gcache" "github.com/go-openapi/runtime/middleware" "github.com/massalabs/station-massa-wallet/api/server/models" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" + "github.com/massalabs/station-massa-wallet/pkg/cache" "github.com/massalabs/station-massa-wallet/pkg/config" "github.com/massalabs/station-massa-wallet/pkg/prompt" "github.com/massalabs/station-massa-wallet/pkg/utils" @@ -23,13 +23,12 @@ type SignRulePromptData struct { SignRule config.SignRule } -func NewAddSignRuleHandler(prompterApp prompt.WalletPrompterInterface, gc gcache.Cache) operations.AddSignRuleHandler { - return &addSignRuleHandler{gc: gc, prompterApp: prompterApp} +func NewAddSignRuleHandler(prompterApp prompt.WalletPrompterInterface) operations.AddSignRuleHandler { + return &addSignRuleHandler{prompterApp: prompterApp} } type addSignRuleHandler struct { prompterApp prompt.WalletPrompterInterface - gc gcache.Cache } func (w *addSignRuleHandler) Handle(params operations.AddSignRuleParams) middleware.Responder { @@ -68,7 +67,7 @@ func (w *addSignRuleHandler) Handle(params operations.AddSignRuleParams) middlew } if cfg.HasEnabledRule(acc.Nickname) { - err = CachePrivateKeyFromPassword(w.gc, acc, password) + err = cache.CachePrivateKeyFromPassword(acc, password) if err != nil { return newErrorResponse(err.Error(), errorCachePrivateKey, http.StatusInternalServerError) } diff --git a/internal/handler/wallet/sign_rule_delete.go b/internal/handler/wallet/sign_rule_delete.go index 5bc9d0de0..ae6bda25e 100644 --- a/internal/handler/wallet/sign_rule_delete.go +++ b/internal/handler/wallet/sign_rule_delete.go @@ -4,7 +4,6 @@ import ( "fmt" "net/http" - "github.com/bluele/gcache" "github.com/go-openapi/runtime/middleware" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" @@ -15,13 +14,12 @@ import ( "github.com/pkg/errors" ) -func NewDeleteSignRuleHandler(prompterApp prompt.WalletPrompterInterface, gc gcache.Cache) operations.DeleteSignRuleHandler { - return &deleteSignRuleHandler{gc: gc, prompterApp: prompterApp} +func NewDeleteSignRuleHandler(prompterApp prompt.WalletPrompterInterface) operations.DeleteSignRuleHandler { + return &deleteSignRuleHandler{prompterApp: prompterApp} } type deleteSignRuleHandler struct { prompterApp prompt.WalletPrompterInterface - gc gcache.Cache } func (w *deleteSignRuleHandler) Handle(params operations.DeleteSignRuleParams) middleware.Responder { diff --git a/internal/handler/wallet/sign_rule_test.go b/internal/handler/wallet/sign_rule_test.go index ad0dd5c04..e11ecce38 100644 --- a/internal/handler/wallet/sign_rule_test.go +++ b/internal/handler/wallet/sign_rule_test.go @@ -8,6 +8,7 @@ import ( "github.com/massalabs/station-massa-wallet/api/server/models" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" + "github.com/massalabs/station-massa-wallet/pkg/cache" assertLib "github.com/stretchr/testify/assert" ) @@ -62,7 +63,7 @@ func Test_signrule_Handlers(t *testing.T) { assert.NotEmpty(addRuleResponse.ID) // check that privateKey is cached - pkey, err := privateKeyFromCache(testCache, account) + pkey, err := cache.PrivateKeyFromCache(account) assert.NoError(err) assert.NotNil(pkey) }) diff --git a/internal/handler/wallet/sign_rule_update.go b/internal/handler/wallet/sign_rule_update.go index ca3e703ed..3f1a63a9f 100644 --- a/internal/handler/wallet/sign_rule_update.go +++ b/internal/handler/wallet/sign_rule_update.go @@ -4,11 +4,11 @@ import ( "fmt" "net/http" - "github.com/bluele/gcache" "github.com/go-openapi/runtime/middleware" "github.com/massalabs/station-massa-wallet/api/server/models" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" + "github.com/massalabs/station-massa-wallet/pkg/cache" "github.com/massalabs/station-massa-wallet/pkg/config" "github.com/massalabs/station-massa-wallet/pkg/prompt" "github.com/massalabs/station-massa-wallet/pkg/utils" @@ -16,13 +16,12 @@ import ( "github.com/pkg/errors" ) -func NewUpdateSignRuleHandler(prompterApp prompt.WalletPrompterInterface, gc gcache.Cache) operations.UpdateSignRuleHandler { - return &updateSignRuleHandler{gc: gc, prompterApp: prompterApp} +func NewUpdateSignRuleHandler(prompterApp prompt.WalletPrompterInterface) operations.UpdateSignRuleHandler { + return &updateSignRuleHandler{prompterApp: prompterApp} } type updateSignRuleHandler struct { prompterApp prompt.WalletPrompterInterface - gc gcache.Cache } func (w *updateSignRuleHandler) Handle(params operations.UpdateSignRuleParams) middleware.Responder { @@ -66,7 +65,7 @@ func (w *updateSignRuleHandler) Handle(params operations.UpdateSignRuleParams) m } if cfg.HasEnabledRule(acc.Nickname) { - err = CachePrivateKeyFromPassword(w.gc, acc, password) + err = cache.CachePrivateKeyFromPassword(acc, password) if err != nil { return newErrorResponse(err.Error(), errorCachePrivateKey, http.StatusInternalServerError) } diff --git a/internal/handler/wallet/sign_test.go b/internal/handler/wallet/sign_test.go index 2328b5b74..5d26fa681 100644 --- a/internal/handler/wallet/sign_test.go +++ b/internal/handler/wallet/sign_test.go @@ -11,6 +11,7 @@ import ( "github.com/massalabs/station-massa-wallet/api/server/models" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" + "github.com/massalabs/station-massa-wallet/pkg/cache" "github.com/massalabs/station-massa-wallet/pkg/config" "github.com/massalabs/station-massa-wallet/pkg/utils" "github.com/stretchr/testify/assert" @@ -211,7 +212,7 @@ func Test_walletSign_Handle(t *testing.T) { checkResultChannel(t, result, true, "") // check that privateKey is cached - pkey, err := privateKeyFromCache(testCache, account) + pkey, err := cache.PrivateKeyFromCache(account) assert.NoError(t, err) assert.NotNil(t, pkey) @@ -262,7 +263,7 @@ func Test_walletSign_Handle(t *testing.T) { checkResultChannel(t, result, true, "") // check that privateKey is cached - pkey, err := privateKeyFromCache(testCache, account) + pkey, err := cache.PrivateKeyFromCache(account) assert.NoError(t, err) assert.NotNil(t, pkey) @@ -319,7 +320,6 @@ func Test_walletSign_Handle(t *testing.T) { res <- (<-resChan) }(testResult) - fmt.Println(">>>>Sign transaction password needed") resp := signTransaction(t, api, nickname, transactionData) verifyStatusCode(t, resp, http.StatusOK) @@ -328,7 +328,7 @@ func Test_walletSign_Handle(t *testing.T) { checkResultChannel(t, result, true, "") // check that privateKey is cached - pkey, err := privateKeyFromCache(testCache, account) + pkey, err := cache.PrivateKeyFromCache(account) assert.NoError(t, err) assert.NotNil(t, pkey) @@ -342,8 +342,6 @@ func Test_walletSign_Handle(t *testing.T) { res <- (<-resChan) }(testResult) - fmt.Println(">>>>Sign transaction password NOT needed") - resp = signTransaction(t, api, nickname, transactionData) verifyStatusCode(t, resp, http.StatusOK) @@ -394,7 +392,7 @@ func Test_walletSign_Handle(t *testing.T) { checkResultChannel(t, result, true, "") // check that privateKey is cached - pkey, err := privateKeyFromCache(testCache, account) + pkey, err := cache.PrivateKeyFromCache(account) assert.NoError(t, err) assert.NotNil(t, pkey) @@ -459,7 +457,7 @@ func Test_walletSign_Handle(t *testing.T) { checkResultChannel(t, result, true, "") // check that privateKey is cached - pkey, err := privateKeyFromCache(testCache, account) + pkey, err := cache.PrivateKeyFromCache(account) assert.NoError(t, err) assert.NotNil(t, pkey) diff --git a/internal/handler/wallet/transfer.go b/internal/handler/wallet/transfer.go index 44abf93a2..91f91e703 100644 --- a/internal/handler/wallet/transfer.go +++ b/internal/handler/wallet/transfer.go @@ -6,11 +6,11 @@ import ( "strconv" "github.com/awnumar/memguard" - "github.com/bluele/gcache" "github.com/go-openapi/runtime/middleware" "github.com/massalabs/station-massa-wallet/api/server/models" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" + "github.com/massalabs/station-massa-wallet/pkg/cache" "github.com/massalabs/station-massa-wallet/pkg/config" "github.com/massalabs/station-massa-wallet/pkg/network" "github.com/massalabs/station-massa-wallet/pkg/prompt" @@ -20,14 +20,13 @@ import ( "github.com/massalabs/station/pkg/node/sendoperation/transaction" ) -func NewTransferCoin(prompterApp prompt.WalletPrompterInterface, massaClient network.NodeFetcherInterface, gc gcache.Cache) operations.TransferCoinHandler { - return &transferCoin{prompterApp: prompterApp, massaClient: massaClient, gc: gc} +func NewTransferCoin(prompterApp prompt.WalletPrompterInterface, massaClient network.NodeFetcherInterface) operations.TransferCoinHandler { + return &transferCoin{prompterApp: prompterApp, massaClient: massaClient} } type transferCoin struct { prompterApp prompt.WalletPrompterInterface massaClient network.NodeFetcherInterface - gc gcache.Cache } func (t *transferCoin) Handle(params operations.TransferCoinParams) middleware.Responder { @@ -113,7 +112,7 @@ func (t *transferCoin) Handle(params operations.TransferCoinParams) middleware.R cfg := config.Get() if cfg.HasEnabledRule(acc.Nickname) { - err = CachePrivateKey(t.gc, acc, output.Password) + err = cache.CachePrivateKey(acc, output.Password) if err != nil { return newErrorResponse(err.Error(), errorCachePrivateKey, http.StatusInternalServerError) } diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 354ade26e..f6fdecc0f 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -36,13 +36,13 @@ const ( cacheSize = 100 ) -func CachePrivateKeyFromPassword(gc gcache.Cache, account *account.Account, password *memguard.LockedBuffer) error { +func CachePrivateKeyFromPassword(account *account.Account, password *memguard.LockedBuffer) error { privateKey, err := account.PrivateKeyBytesInClear(password) if err != nil { return fmt.Errorf("error caching private key: %w", err) } - err = CachePrivateKey(gc, account, privateKey) + err = CachePrivateKey(account, privateKey) if err != nil { return fmt.Errorf("error caching private key: %w", err) } @@ -50,7 +50,7 @@ func CachePrivateKeyFromPassword(gc gcache.Cache, account *account.Account, pass return nil } -func CachePrivateKey(gc gcache.Cache, account *account.Account, privateKey *memguard.LockedBuffer) error { +func CachePrivateKey(account *account.Account, privateKey *memguard.LockedBuffer) error { cacheKey, err := privateKeyCacheKey(account) if err != nil { return fmt.Errorf("%w: %w", utils.ErrPrivateKeyCache, err) @@ -71,7 +71,7 @@ func CachePrivateKey(gc gcache.Cache, account *account.Account, privateKey *memg copy(cacheValue, cipheredPrivateKey.Bytes()) cipheredPrivateKey.Destroy() - err = gc.SetWithExpire(key, cacheValue, expirationDuration()) + err = cache.SetWithExpire(key, cacheValue, expirationDuration()) if err != nil { return fmt.Errorf("error set private key in cache: %w", err) } @@ -81,7 +81,6 @@ func CachePrivateKey(gc gcache.Cache, account *account.Account, privateKey *memg // privateKeyFromCache return the private key from the cache or an error. func PrivateKeyFromCache( - gc gcache.Cache, acc *account.Account, ) (*memguard.LockedBuffer, error) { cacheKey, err := privateKeyCacheKey(acc) @@ -91,7 +90,7 @@ func PrivateKeyFromCache( keyHash := KeyHash([]byte(cacheKey)) - value, err := gc.Get(keyHash) + value, err := cache.Get(keyHash) if err != nil { if err.Error() == gcache.KeyNotFoundError.Error() { return nil, fmt.Errorf("%w: %w", utils.ErrPrivateKeyCache, err) From af30a139e77231a5d2fa1334046ee7628f85e8d8 Mon Sep 17 00:00:00 2001 From: Peterjah Date: Tue, 21 Jan 2025 13:38:02 +0100 Subject: [PATCH 07/10] add signrule endpoints tests --- cmd/massa-wallet/massa-wallet.go | 5 +- internal/handler/wallet/api_test.go | 6 +- .../handler/wallet/get_all_assets_test.go | 14 +- internal/handler/wallet/sign_rule_test.go | 226 ++++++++++++++---- pkg/assets/asset.go | 18 +- pkg/assets/asset_test.go | 15 +- pkg/cache/cache.go | 2 +- pkg/config/rules.go | 11 +- 8 files changed, 227 insertions(+), 70 deletions(-) diff --git a/cmd/massa-wallet/massa-wallet.go b/cmd/massa-wallet/massa-wallet.go index 36698ecae..802f10142 100644 --- a/cmd/massa-wallet/massa-wallet.go +++ b/cmd/massa-wallet/massa-wallet.go @@ -8,6 +8,7 @@ import ( "github.com/massalabs/station-massa-wallet/internal/handler" walletApp "github.com/massalabs/station-massa-wallet/pkg/app" "github.com/massalabs/station-massa-wallet/pkg/assets" + "github.com/massalabs/station-massa-wallet/pkg/cache" "github.com/massalabs/station-massa-wallet/pkg/config" "github.com/massalabs/station-massa-wallet/pkg/network" "github.com/massalabs/station-massa-wallet/pkg/prompt" @@ -24,7 +25,9 @@ func StartServer(app *walletApp.WalletApp) { promptApp = prompt.NewEnvPrompter(app) } - _, err := assets.InitAssetsStore("", massaClient) + cache.Init() + + _, err := assets.InitAssetsStore(massaClient) if err != nil { logger.Fatalf("Failed to create AssetsStore: %v", err) } diff --git a/internal/handler/wallet/api_test.go b/internal/handler/wallet/api_test.go index edf7bdb52..3771c561b 100644 --- a/internal/handler/wallet/api_test.go +++ b/internal/handler/wallet/api_test.go @@ -78,7 +78,9 @@ func MockAPI() (*operations.MassaWalletAPI, chan walletapp.EventData, error) { nodeFetcher := network.NewNodeFetcher() - testAssetStore, err = assets.InitAssetsStore(walletPath, nodeFetcher) + assets.SetFileDirOverride(walletPath) + + testAssetStore, err = assets.InitAssetsStore(nodeFetcher) if err != nil { log.Fatalf("Failed to create AssetsStore: %v", err) } @@ -89,7 +91,7 @@ func MockAPI() (*operations.MassaWalletAPI, chan walletapp.EventData, error) { config.SetConfigFileDirOverride(walletPath) config.Load() - testCache = cache.Get() + testCache = cache.Init() // Set wallet API endpoints AppendEndpoints(massaWalletAPI, prompterAppMock, massaNodeMock) diff --git a/internal/handler/wallet/get_all_assets_test.go b/internal/handler/wallet/get_all_assets_test.go index c8df4c46d..a34ad4ebf 100644 --- a/internal/handler/wallet/get_all_assets_test.go +++ b/internal/handler/wallet/get_all_assets_test.go @@ -4,13 +4,11 @@ import ( "encoding/json" "fmt" "net/http" - "os" "testing" "github.com/massalabs/station-massa-wallet/api/server/models" "github.com/massalabs/station-massa-wallet/api/server/restapi/operations" "github.com/massalabs/station-massa-wallet/pkg/assets" - "github.com/massalabs/station-massa-wallet/pkg/network" "github.com/stretchr/testify/assert" ) @@ -63,12 +61,12 @@ func assertAssetInfoWithBalanceEqual(t *testing.T, actual, expected *models.Asse } func getExpectedAssetsCount(t *testing.T) int { - tempDir, err := os.MkdirTemp(os.TempDir(), "*-wallet-dir") - assert.NoError(t, err) - nodeFetcher := network.NewNodeFetcher() - store, err := assets.InitAssetsStore(tempDir, nodeFetcher) - assert.NoError(t, err) - defaultAssets, err := store.Default() + // tempDir, err := os.MkdirTemp(os.TempDir(), "*-wallet-dir") + // assert.NoError(t, err) + // nodeFetcher := network.NewNodeFetcher() + // store, err := assets.InitAssetsStore(tempDir, nodeFetcher) + // assert.NoError(t, err) + defaultAssets, err := assets.Store.Default() assert.NoError(t, err) counter := 0 diff --git a/internal/handler/wallet/sign_rule_test.go b/internal/handler/wallet/sign_rule_test.go index e11ecce38..d2ec49ab3 100644 --- a/internal/handler/wallet/sign_rule_test.go +++ b/internal/handler/wallet/sign_rule_test.go @@ -9,11 +9,12 @@ import ( "github.com/massalabs/station-massa-wallet/api/server/models" walletapp "github.com/massalabs/station-massa-wallet/pkg/app" "github.com/massalabs/station-massa-wallet/pkg/cache" - assertLib "github.com/stretchr/testify/assert" + "github.com/massalabs/station-massa-wallet/pkg/config" + "github.com/stretchr/testify/assert" ) func Test_signrule_Handlers(t *testing.T) { - assert := assertLib.New(t) + assert := assert.New(t) api, resChan, err := MockAPI() assert.NoError(err) @@ -21,18 +22,30 @@ func Test_signrule_Handlers(t *testing.T) { addhandler, exist := api.HandlerFor("post", "/api/accounts/{nickname}/signrules") assert.True(exist) + updatehandler, exist := api.HandlerFor("put", "/api/accounts/{nickname}/signrules/{ruleId}") + assert.True(exist) + + deletehandler, exist := api.HandlerFor("delete", "/api/accounts/{nickname}/signrules/{ruleId}") + assert.True(exist) + nickname := "zeWahLetName" password := "zePassword" account := createAccount(password, nickname, t, prompterAppMock) + cfg := config.Get() + t.Run("Add sign rule", func(t *testing.T) { - body := `{ - "name": "Test Rule", - "contract": "Test Contract", - "ruleType": "auto_sign", - "enabled": true, + contract := "TestAddContract" + ruleType := config.RuleTypeAutoSign + ruleName := "Test Rule" + enabled := true + body := fmt.Sprintf(`{ + "name": "%s", + "contract": "%s", + "ruleType": "%s", + "enabled": %t, "description": "Test Description" - }` + }`, ruleName, contract, ruleType, enabled) testResult := make(chan walletapp.EventData) @@ -43,7 +56,6 @@ func Test_signrule_Handlers(t *testing.T) { Message: password, } - // forward test result to test goroutine res <- (<-resChan) }(testResult) @@ -62,47 +74,167 @@ func Test_signrule_Handlers(t *testing.T) { assert.NoError(err) assert.NotEmpty(addRuleResponse.ID) + // check rule is added + rulePtr := cfg.GetEnabledRuleForContract(account.Nickname, &contract) + assert.NotNil(rulePtr) + assert.Equal(*rulePtr, ruleType) + + hasRule := cfg.HasEnabledRule(account.Nickname) + assert.True(hasRule) + + // check rule contains expected fields + rule, err := cfg.GetSignRule(account.Nickname, addRuleResponse.ID) + assert.NoError(err) + assert.Equal(rule.Name, ruleName) + assert.Equal(rule.Contract, contract) + assert.Equal(rule.RuleType, ruleType) + assert.Equal(rule.Enabled, enabled) + // check that privateKey is cached pkey, err := cache.PrivateKeyFromCache(account) assert.NoError(err) assert.NotNil(pkey) }) -} -// func TestDeleteSignRule(t *testing.T) { -// api, _, _, resultChannel, err := MockAPI() -// assert.NoError(t, err) - -// body := `{ -// "nickname": "Test Nickname", -// "ruleID": "Test Rule ID" -// }` - -// resp, err := processHTTPRequest(api, "DELETE", "/sign-rule", body) -// assert.NoError(t, err) -// assert.Equal(t, http.StatusOK, resp.Code) - -// result := <-resultChannel -// checkResultChannel(t, result, true, "Sign rule deleted successfully") -// } - -// func TestUpdateSignRule(t *testing.T) { -// api, _, _, resultChannel, err := MockAPI() -// assert.NoError(t, err) - -// body := `{ -// "nickname": "Test Nickname", -// "ruleID": "Test Rule ID", -// "name": "Updated Rule", -// "contract": "Updated Contract", -// "ruleType": "Updated Type", -// "enabled": false -// }` - -// resp, err := processHTTPRequest(api, "PUT", "/sign-rule", body) -// assert.NoError(t, err) -// assert.Equal(t, http.StatusOK, resp.Code) - -// result := <-resultChannel -// checkResultChannel(t, result, true, "Sign rule updated successfully") -// } + t.Run("Update sign rule", func(t *testing.T) { + contract := "*" + ruleType := config.RuleTypeDisablePasswordPrompt + ruleName := "Test Rule" + enabled := true + body := fmt.Sprintf(`{ + "name": "%s", + "contract": "%s", + "ruleType": "%s", + "enabled": %t, + "description": "Test Description" + }`, ruleName, contract, ruleType, enabled) + + testResult := make(chan walletapp.EventData) + + // Send password to prompter app and wait for result + go func(res chan walletapp.EventData) { + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ + BaseMessage: walletapp.BaseMessage{}, + Message: password, + } + + res <- (<-resChan) + }(testResult) + + // Add a sign rule first + resp, err := handleHTTPRequest(addhandler, "POST", fmt.Sprintf("/api/accounts/%s/signrules", nickname), body) + assert.NoError(err) + verifyStatusCode(t, resp, http.StatusOK) + + var addRuleResponse models.AddSignRuleResponse + err = json.Unmarshal(resp.Body.Bytes(), &addRuleResponse) + assert.NoError(err) + assert.NotEmpty(addRuleResponse.ID) + + // Update the sign rule + updatedRuleName := "Updated Test Rule" + enabled = false + updatedBody := fmt.Sprintf(`{ + "name": "%s", + "contract": "%s", + "ruleType": "%s", + "enabled": %t, + "description": "Updated Test Description" + }`, updatedRuleName, contract, ruleType, enabled) + + // Send password to prompter app and wait for result + go func(res chan walletapp.EventData) { + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ + BaseMessage: walletapp.BaseMessage{}, + Message: password, + } + + res <- (<-resChan) + }(testResult) + + resp, err = handleHTTPRequest(updatehandler, "PUT", fmt.Sprintf("/api/accounts/%s/signrules/%s", nickname, addRuleResponse.ID), updatedBody) + assert.NoError(err) + verifyStatusCode(t, resp, http.StatusOK) + + var updateRuleResponse models.UpdateSignRuleResponse + err = json.Unmarshal(resp.Body.Bytes(), &updateRuleResponse) + assert.NoError(err) + assert.NotEmpty(updateRuleResponse.ID) + + // Check rule is updated + rule, err := cfg.GetSignRule(account.Nickname, updateRuleResponse.ID) + assert.NoError(err) + assert.Equal(rule.Name, updatedRuleName) + assert.Equal(rule.Contract, contract) + assert.Equal(rule.RuleType, ruleType) + assert.Equal(rule.Enabled, enabled) + + // Check that privateKey is still cached + pkey, err := cache.PrivateKeyFromCache(account) + assert.NoError(err) + assert.NotNil(pkey) + }) + + t.Run("Delete sign rule", func(t *testing.T) { + contract := "TestDeleteContract" + ruleType := config.RuleTypeAutoSign + ruleName := "Test Rule" + enabled := true + body := fmt.Sprintf(`{ + "name": "%s", + "contract": "%s", + "ruleType": "%s", + "enabled": %t, + "description": "Test Description" + }`, ruleName, contract, ruleType, enabled) + + testResult := make(chan walletapp.EventData) + + // Send password to prompter app and wait for result + go func(res chan walletapp.EventData) { + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ + BaseMessage: walletapp.BaseMessage{}, + Message: password, + } + + res <- (<-resChan) + }(testResult) + + // Add a sign rule first + resp, err := handleHTTPRequest(addhandler, "POST", fmt.Sprintf("/api/accounts/%s/signrules", nickname), body) + assert.NoError(err) + verifyStatusCode(t, resp, http.StatusOK) + + var addRuleResponse models.AddSignRuleResponse + err = json.Unmarshal(resp.Body.Bytes(), &addRuleResponse) + assert.NoError(err) + assert.NotEmpty(addRuleResponse.ID) + + // Send password to prompter app and wait for result + go func(res chan walletapp.EventData) { + prompterAppMock.App().PromptInput <- &walletapp.StringPromptInput{ + BaseMessage: walletapp.BaseMessage{}, + Message: password, + } + + res <- (<-resChan) + }(testResult) + + // Now delete the sign rule + resp, err = handleHTTPRequest(deletehandler, "DELETE", fmt.Sprintf("/api/accounts/%s/signrules/%s", nickname, addRuleResponse.ID), "") + assert.NoError(err) + verifyStatusCode(t, resp, http.StatusOK) + + // Check rule is deleted + rulePtr := cfg.GetEnabledRuleForContract(account.Nickname, &contract) + assert.Nil(rulePtr) + + hasRule := cfg.HasEnabledRule(account.Nickname) + assert.True(hasRule) + + // Check that privateKey is still cached + pkey, err := cache.PrivateKeyFromCache(account) + assert.NoError(err) + assert.NotNil(pkey) + }) +} diff --git a/pkg/assets/asset.go b/pkg/assets/asset.go index d9fcc7851..57fffd8f3 100644 --- a/pkg/assets/asset.go +++ b/pkg/assets/asset.go @@ -55,24 +55,27 @@ type assetData struct { ChainID int64 `json:"chainID"` } -var Store *AssetsStore +var ( + Store *AssetsStore + filePathOverride string +) // NewAssetsStore creates and initializes a new instance of AssetsStore. // If assetsJSONDir is empty, it will use the default wallet path. -func InitAssetsStore(assetsJSONDir string, massaClient *network.NodeFetcher) (*AssetsStore, error) { +func InitAssetsStore(massaClient *network.NodeFetcher) (*AssetsStore, error) { Store = &AssetsStore{ Assets: make(map[string]Assets), massaClient: massaClient, } - if assetsJSONDir == "" { + if filePathOverride != "" { + Store.assetsJSONDir = filePathOverride + } else { assetsJSONDir, err := wallet.Path() if err != nil { return nil, errors.Wrap(err, "Failed to get AssetsStore JSON file") } Store.assetsJSONDir = assetsJSONDir - } else { - Store.assetsJSONDir = assetsJSONDir } if err := Store.loadAccountsStore(); err != nil { @@ -87,6 +90,11 @@ func InitAssetsStore(assetsJSONDir string, massaClient *network.NodeFetcher) (*A return Store, nil } +// Used by unit test +func SetFileDirOverride(path string) { + filePathOverride = path +} + // loadAccountsStore loads the data from the assets JSON file into the AssetsStore. func (s *AssetsStore) loadAccountsStore() error { // Check if the file exists, and if not, create a new one with an empty object diff --git a/pkg/assets/asset_test.go b/pkg/assets/asset_test.go index 81cd6127e..3b8a304bc 100644 --- a/pkg/assets/asset_test.go +++ b/pkg/assets/asset_test.go @@ -42,7 +42,10 @@ func TestLoadAccountsStore(t *testing.T) { // Create a new instance of AssetsStore and load data from the testing file nodeFetcher := network.NewNodeFetcher() - _, err = InitAssetsStore(tempDir, nodeFetcher) + + SetFileDirOverride(tempDir) + + _, err = InitAssetsStore(nodeFetcher) assert.NoError(t, err) // Validate the loaded data @@ -64,7 +67,10 @@ func TestAssetExists(t *testing.T) { // Create a new instance of AssetsStore and load data from the testing file nodeFetcher := network.NewNodeFetcher() - _, err = InitAssetsStore(tempDir, nodeFetcher) + + SetFileDirOverride(tempDir) + + _, err = InitAssetsStore(nodeFetcher) assert.NoError(t, err) // Test case 1: Check for an existing asset @@ -109,7 +115,10 @@ func TestAddAndDeleteAsset(t *testing.T) { // Create a new instance of AssetsStore and load data from the testing file nodeFetcher := network.NewNodeFetcher() - _, err = InitAssetsStore(tempDir, nodeFetcher) + + SetFileDirOverride(tempDir) + + _, err = InitAssetsStore(nodeFetcher) assert.NoError(t, err) // Test case 1: Add an asset and check if it's saved to JSON diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index f6fdecc0f..2ab081c04 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -19,7 +19,7 @@ var ( once sync.Once ) -func Get() gcache.Cache { +func Init() gcache.Cache { once.Do(func() { cache = gcache.New(cacheSize). LRU(). diff --git a/pkg/config/rules.go b/pkg/config/rules.go index 105c69f30..188f59788 100644 --- a/pkg/config/rules.go +++ b/pkg/config/rules.go @@ -240,22 +240,27 @@ func (c *Config) GetEnabledRuleForContract(accountName string, contract *string) return nil } + var ruleType *RuleType = nil + // Check if there is any enabled rule that applies to the contract for _, rule := range account.SignRules { if rule.Enabled { switch rule.RuleType { case RuleTypeAutoSign: if contract != nil && rule.Contract == *contract { - return &rule.RuleType + ruleType = &rule.RuleType } case RuleTypeDisablePasswordPrompt: if rule.Contract == "*" || (contract != nil && rule.Contract == *contract) { - return &rule.RuleType + if ruleType == nil { + // If there are multiple rules that apply to the contract, the rule with the highest priority is used (AutoSign) + ruleType = &rule.RuleType + } } } } } - return nil + return ruleType } From f3b819df7693cb317c7e0ec54599d222877d92ee Mon Sep 17 00:00:00 2001 From: Peterjah Date: Tue, 21 Jan 2025 15:12:12 +0100 Subject: [PATCH 08/10] factorize findIndex rule --- api/server/models/sign_rule.go | 62 +++++++++++++-------------- api/server/restapi/embedded_spec.go | 12 +----- api/walletApi-V0.yml | 5 +-- internal/handler/wallet/get_config.go | 2 +- pkg/config/rules.go | 60 +++++++++++--------------- 5 files changed, 59 insertions(+), 82 deletions(-) diff --git a/api/server/models/sign_rule.go b/api/server/models/sign_rule.go index 40bc7d779..674efa30e 100644 --- a/api/server/models/sign_rule.go +++ b/api/server/models/sign_rule.go @@ -7,7 +7,6 @@ package models import ( "context" - "encoding/json" "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" @@ -28,8 +27,7 @@ type SignRule struct { // rule type // Required: true - // Enum: ["password_prompt","auto_sign"] - RuleType *string `json:"ruleType"` + RuleType RuleType `json:"ruleType"` } // Validate validates this sign rule @@ -46,51 +44,49 @@ func (m *SignRule) Validate(formats strfmt.Registry) error { return nil } -var signRuleTypeRuleTypePropEnum []interface{} +func (m *SignRule) validateRuleType(formats strfmt.Registry) error { -func init() { - var res []string - if err := json.Unmarshal([]byte(`["password_prompt","auto_sign"]`), &res); err != nil { - panic(err) - } - for _, v := range res { - signRuleTypeRuleTypePropEnum = append(signRuleTypeRuleTypePropEnum, v) + if err := validate.Required("ruleType", "body", RuleType(m.RuleType)); err != nil { + return err } -} - -const ( - - // SignRuleRuleTypePasswordPrompt captures enum value "password_prompt" - SignRuleRuleTypePasswordPrompt string = "password_prompt" - // SignRuleRuleTypeAutoSign captures enum value "auto_sign" - SignRuleRuleTypeAutoSign string = "auto_sign" -) - -// prop value enum -func (m *SignRule) validateRuleTypeEnum(path, location string, value string) error { - if err := validate.EnumCase(path, location, value, signRuleTypeRuleTypePropEnum, true); err != nil { + if err := m.RuleType.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("ruleType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("ruleType") + } return err } + return nil } -func (m *SignRule) validateRuleType(formats strfmt.Registry) error { +// ContextValidate validate this sign rule based on the context it is used +func (m *SignRule) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error - if err := validate.Required("ruleType", "body", m.RuleType); err != nil { - return err + if err := m.contextValidateRuleType(ctx, formats); err != nil { + res = append(res, err) } - // value enum - if err := m.validateRuleTypeEnum("ruleType", "body", *m.RuleType); err != nil { - return err + if len(res) > 0 { + return errors.CompositeValidationError(res...) } - return nil } -// ContextValidate validates this sign rule based on context it is used -func (m *SignRule) ContextValidate(ctx context.Context, formats strfmt.Registry) error { +func (m *SignRule) contextValidateRuleType(ctx context.Context, formats strfmt.Registry) error { + + if err := m.RuleType.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("ruleType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("ruleType") + } + return err + } + return nil } diff --git a/api/server/restapi/embedded_spec.go b/api/server/restapi/embedded_spec.go index 7620731dc..e11d29490 100644 --- a/api/server/restapi/embedded_spec.go +++ b/api/server/restapi/embedded_spec.go @@ -1348,11 +1348,7 @@ func init() { "default": true }, "ruleType": { - "type": "string", - "enum": [ - "password_prompt", - "auto_sign" - ] + "$ref": "#/definitions/RuleType" } } }, @@ -2880,11 +2876,7 @@ func init() { "default": true }, "ruleType": { - "type": "string", - "enum": [ - "password_prompt", - "auto_sign" - ] + "$ref": "#/definitions/RuleType" } } }, diff --git a/api/walletApi-V0.yml b/api/walletApi-V0.yml index b9d075c38..34ed432a8 100644 --- a/api/walletApi-V0.yml +++ b/api/walletApi-V0.yml @@ -963,10 +963,7 @@ definitions: type: boolean default: true ruleType: - type: string - enum: - - password_prompt - - auto_sign + $ref: "#/definitions/RuleType" required: - ruleType diff --git a/internal/handler/wallet/get_config.go b/internal/handler/wallet/get_config.go index 5f4769cd5..efb3cfb3c 100644 --- a/internal/handler/wallet/get_config.go +++ b/internal/handler/wallet/get_config.go @@ -37,7 +37,7 @@ func newConfigModel(cfg *config.Config) (*models.Config, error) { modelSignRules[i] = &models.SignRule{ Contract: &rule.Contract, Enabled: &rule.Enabled, - RuleType: (*string)(&rule.RuleType), + RuleType: (models.RuleType)(rule.RuleType), } } modelAccounts[nickname] = models.AccountConfig{ diff --git a/pkg/config/rules.go b/pkg/config/rules.go index 188f59788..140921c02 100644 --- a/pkg/config/rules.go +++ b/pkg/config/rules.go @@ -41,35 +41,36 @@ func (c *Config) AddSignRule(accountName string, rule SignRule) (string, error) return rule.ID, nil } +func (c *Config) findRuleIndex(accountConfig AccountCfg, ruleID string) (int, error) { + for i, rule := range accountConfig.SignRules { + if rule.ID == ruleID { + return i, nil + } + } + + return -1, fmt.Errorf("rule not found: %s", ruleID) +} + func (c *Config) DeleteSignRule(accountName, ruleID string) error { configManager.mu.Lock() defer configManager.mu.Unlock() // Check if the account exists - account, exists := c.Accounts[accountName] + accountConfig, exists := c.Accounts[accountName] if !exists { return fmt.Errorf("account not found: %s", accountName) } - // Find the index of the rule to delete - index := -1 - - for i, rule := range account.SignRules { - if rule.ID == ruleID { - index = i - break - } - } - - if index == -1 { - return fmt.Errorf("rule not found: %s", ruleID) + index, err := c.findRuleIndex(accountConfig, ruleID) + if err != nil { + return fmt.Errorf("deleting sign rule: %w", err) } // Remove the rule from the slice - account.SignRules = append(account.SignRules[:index], account.SignRules[index+1:]...) - c.Accounts[accountName] = account + accountConfig.SignRules = append(accountConfig.SignRules[:index], accountConfig.SignRules[index+1:]...) + c.Accounts[accountName] = accountConfig - err := saveConfigUnsafe(c) + err = saveConfigUnsafe(c) if err != nil { return fmt.Errorf("error saving configuration: %v", err) } @@ -82,35 +83,26 @@ func (c *Config) UpdateSignRule(accountName, ruleID string, newRule SignRule) (s defer configManager.mu.Unlock() // Check if the account exists - account, exists := c.Accounts[accountName] + accountConfig, exists := c.Accounts[accountName] if !exists { return "", fmt.Errorf("account not found: %s", accountName) } - // Find the index of the rule to update - index := -1 - - for i, rule := range account.SignRules { - if rule.ID == ruleID { - index = i - break - } - } - - if index == -1 { - return "", fmt.Errorf("rule not found: %s", ruleID) + index, err := c.findRuleIndex(accountConfig, ruleID) + if err != nil { + return "", fmt.Errorf("updating sign rule: %w", err) } // Delete the existing rule - account.SignRules = append(account.SignRules[:index], account.SignRules[index+1:]...) - c.Accounts[accountName] = account + accountConfig.SignRules = append(accountConfig.SignRules[:index], accountConfig.SignRules[index+1:]...) + c.Accounts[accountName] = accountConfig // Add the new rule newRule.ID = generateRuleID(accountName, newRule) - account.SignRules = append(account.SignRules, newRule) - c.Accounts[accountName] = account + accountConfig.SignRules = append(accountConfig.SignRules, newRule) + c.Accounts[accountName] = accountConfig - err := saveConfigUnsafe(c) + err = saveConfigUnsafe(c) if err != nil { return "", fmt.Errorf("error saving configuration: %v", err) } From a103dae04617cd93d1f3dd2111fb250bd1b806b8 Mon Sep 17 00:00:00 2001 From: Peterjah Date: Tue, 21 Jan 2025 15:42:39 +0100 Subject: [PATCH 09/10] refactor InitAssetsStore --- cmd/massa-wallet/massa-wallet.go | 5 +---- internal/handler/wallet/api_test.go | 5 +---- pkg/assets/asset.go | 10 +++++----- pkg/assets/asset_test.go | 9 +++------ 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/cmd/massa-wallet/massa-wallet.go b/cmd/massa-wallet/massa-wallet.go index 802f10142..179596820 100644 --- a/cmd/massa-wallet/massa-wallet.go +++ b/cmd/massa-wallet/massa-wallet.go @@ -27,10 +27,7 @@ func StartServer(app *walletApp.WalletApp) { cache.Init() - _, err := assets.InitAssetsStore(massaClient) - if err != nil { - logger.Fatalf("Failed to create AssetsStore: %v", err) - } + assets.InitAssetsStore(massaClient) // Initializes API massaWalletAPI, err := handler.InitializeAPI( diff --git a/internal/handler/wallet/api_test.go b/internal/handler/wallet/api_test.go index 3771c561b..38806b861 100644 --- a/internal/handler/wallet/api_test.go +++ b/internal/handler/wallet/api_test.go @@ -80,10 +80,7 @@ func MockAPI() (*operations.MassaWalletAPI, chan walletapp.EventData, error) { assets.SetFileDirOverride(walletPath) - testAssetStore, err = assets.InitAssetsStore(nodeFetcher) - if err != nil { - log.Fatalf("Failed to create AssetsStore: %v", err) - } + testAssetStore = assets.InitAssetsStore(nodeFetcher) massaNodeMock := NewNodeFetcherMock() diff --git a/pkg/assets/asset.go b/pkg/assets/asset.go index 57fffd8f3..e8ec52ba0 100644 --- a/pkg/assets/asset.go +++ b/pkg/assets/asset.go @@ -62,7 +62,7 @@ var ( // NewAssetsStore creates and initializes a new instance of AssetsStore. // If assetsJSONDir is empty, it will use the default wallet path. -func InitAssetsStore(massaClient *network.NodeFetcher) (*AssetsStore, error) { +func InitAssetsStore(massaClient *network.NodeFetcher) *AssetsStore { Store = &AssetsStore{ Assets: make(map[string]Assets), massaClient: massaClient, @@ -73,21 +73,21 @@ func InitAssetsStore(massaClient *network.NodeFetcher) (*AssetsStore, error) { } else { assetsJSONDir, err := wallet.Path() if err != nil { - return nil, errors.Wrap(err, "Failed to get AssetsStore JSON file") + logger.Fatalf("Failed to create AssetsStore: Failed to get AssetsStore JSON file", err) } Store.assetsJSONDir = assetsJSONDir } if err := Store.loadAccountsStore(); err != nil { - return nil, errors.Wrap(err, "failed to create AssetsStore") + logger.Fatalf("Failed to create AssetsStore:", err) } err := Store.InitDefault() if err != nil { - return nil, errors.Wrap(err, "failed to create AssetsStore") + logger.Fatalf("Failed to create AssetsStore:", err) } - return Store, nil + return Store } // Used by unit test diff --git a/pkg/assets/asset_test.go b/pkg/assets/asset_test.go index 3b8a304bc..1aac0a63e 100644 --- a/pkg/assets/asset_test.go +++ b/pkg/assets/asset_test.go @@ -45,8 +45,7 @@ func TestLoadAccountsStore(t *testing.T) { SetFileDirOverride(tempDir) - _, err = InitAssetsStore(nodeFetcher) - assert.NoError(t, err) + InitAssetsStore(nodeFetcher) // Validate the loaded data expectedAccountName := "dummyAccount" @@ -70,8 +69,7 @@ func TestAssetExists(t *testing.T) { SetFileDirOverride(tempDir) - _, err = InitAssetsStore(nodeFetcher) - assert.NoError(t, err) + InitAssetsStore(nodeFetcher) // Test case 1: Check for an existing asset existingNickname := "dummyAccount" @@ -118,8 +116,7 @@ func TestAddAndDeleteAsset(t *testing.T) { SetFileDirOverride(tempDir) - _, err = InitAssetsStore(nodeFetcher) - assert.NoError(t, err) + InitAssetsStore(nodeFetcher) // Test case 1: Add an asset and check if it's saved to JSON nickname := "dummyAccount" From bf376782d68d2e643ca71f088d8c75f57cc84a61 Mon Sep 17 00:00:00 2001 From: Pierre Seznec <34547263+peterjah@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:01:40 +0100 Subject: [PATCH 10/10] Sign rules prompts (#987) * add signature management prompters --- internal/handler/wallet/add_asset.go | 4 +- internal/handler/wallet/sign_rule_add.go | 27 ++- internal/handler/wallet/sign_rule_delete.go | 6 +- internal/handler/wallet/sign_rule_test.go | 43 ++++- internal/handler/wallet/sign_rule_update.go | 25 +-- internal/handler/wallet/sign_test.go | 4 +- internal/handler/wallet/transfer.go | 2 +- pkg/app/enums.go | 51 +++++ pkg/app/events.go | 21 -- pkg/config/config.go | 2 +- pkg/config/config_test.go | 27 ++- pkg/config/rules.go | 43 +++-- pkg/utils/address.go | 20 +- pkg/wails/wails.go | 4 + wails-frontend/package-lock.json | 8 +- wails-frontend/package.json | 2 +- wails-frontend/package.json.md5 | 2 +- wails-frontend/src/app.tsx | 31 +-- wails-frontend/src/events/index.ts | 1 + .../src/events/{events.tsx => types.tsx} | 20 +- wails-frontend/src/i18n/en_US.json | 34 +++- .../pages/ImportPrivateKey/PromptNickname.tsx | 2 +- .../ImportPrivateKey/PromptPrivateKey.tsx | 2 +- .../BuySellRoll/BuySellRoll.tsx | 3 +- .../CallSC/FTTransferInfo.tsx | 3 +- .../pages/PasswordPromptHandler/Delete.tsx | 7 +- .../ExecuteSC.tsx/ExecuteSc.tsx | 3 +- .../PasswordPromptHandler.tsx | 15 +- .../src/pages/PasswordPromptHandler/Sign.tsx | 7 +- .../SignComponentUtils/From.tsx | 5 +- .../SignComponentUtils/FromTo.tsx | 13 +- .../SignComponentUtils/OperationCost.tsx | 2 +- .../Transaction/Transaction.tsx | 3 +- .../components/account.tsx | 28 +++ .../components/clipBoardCopy.tsx | 14 ++ .../pages/SignRuleHandler/signRulePrompt.tsx | 180 ++++++++++++++++++ .../src/pages/SignRuleHandler/types.tsx | 24 +++ wails-frontend/src/pages/backupKeyPairs.tsx | 7 +- wails-frontend/src/pages/backupMethods.tsx | 10 +- wails-frontend/src/pages/confirmDelete.tsx | 7 +- wails-frontend/src/pages/failure.tsx | 2 +- wails-frontend/src/pages/importFile.tsx | 9 +- wails-frontend/src/pages/importMethods.tsx | 2 +- wails-frontend/src/pages/newPassword.tsx | 13 +- wails-frontend/src/pages/success.tsx | 15 +- .../src/utils/handleApplyResult.tsx | 2 +- wails-frontend/wailsjs/go/models.ts | 17 ++ 47 files changed, 582 insertions(+), 190 deletions(-) create mode 100644 pkg/app/enums.go create mode 100644 wails-frontend/src/events/index.ts rename wails-frontend/src/events/{events.tsx => types.tsx} (52%) create mode 100644 wails-frontend/src/pages/PasswordPromptHandler/components/account.tsx create mode 100644 wails-frontend/src/pages/PasswordPromptHandler/components/clipBoardCopy.tsx create mode 100644 wails-frontend/src/pages/SignRuleHandler/signRulePrompt.tsx create mode 100644 wails-frontend/src/pages/SignRuleHandler/types.tsx diff --git a/internal/handler/wallet/add_asset.go b/internal/handler/wallet/add_asset.go index 3379a88c5..2694b4abd 100644 --- a/internal/handler/wallet/add_asset.go +++ b/internal/handler/wallet/add_asset.go @@ -24,9 +24,7 @@ type addAsset struct { } func (a *addAsset) Handle(params operations.AddAssetParams) middleware.Responder { - // Check if the address is valid - if !utils.IsValidAddress(params.AssetAddress) { - // Return an error indicating the address is not valid + if !utils.IsValidContract(params.AssetAddress) { errorMsg := "Invalid address format" return operations.NewAddAssetUnprocessableEntity().WithPayload(&models.Error{Code: errorInvalidAssetAddress, Message: errorMsg}) } diff --git a/internal/handler/wallet/sign_rule_add.go b/internal/handler/wallet/sign_rule_add.go index 9d4436760..9f2a0947b 100644 --- a/internal/handler/wallet/sign_rule_add.go +++ b/internal/handler/wallet/sign_rule_add.go @@ -37,6 +37,23 @@ func (w *addSignRuleHandler) Handle(params operations.AddSignRuleParams) middlew return errResp } + newRule := config.SignRule{ + Name: params.Body.Name, + Contract: *params.Body.Contract, + RuleType: config.RuleType(params.Body.RuleType), + Enabled: *params.Body.Enabled, + } + + cfg := config.Get() + + if exists := cfg.IsExistingRule(acc.Nickname, newRule); exists { + return newErrorResponse("Rule already exists", errorAddSignRule, http.StatusBadRequest) + } + + if err := config.ValidateRule(newRule); err != nil { + return operations.NewAddSignRuleBadRequest().WithPayload(&models.Error{Code: errorInvalidAssetAddress, Message: err.Error()}) + } + promptRequest, err := w.getPromptRequest(params, acc) if err != nil { return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorAddSignRule, http.StatusBadRequest) @@ -52,15 +69,6 @@ func (w *addSignRuleHandler) Handle(params operations.AddSignRuleParams) middlew return newErrorResponse(msg, errorGetWallets, http.StatusInternalServerError) } - cfg := config.Get() - - newRule := config.SignRule{ - Name: params.Body.Name, - Contract: *params.Body.Contract, - RuleType: config.RuleType(params.Body.RuleType), - Enabled: *params.Body.Enabled, - } - ruleID, err := cfg.AddSignRule(acc.Nickname, newRule) if err != nil { return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorAddSignRule, http.StatusInternalServerError) @@ -94,6 +102,7 @@ func (w *addSignRuleHandler) getPromptRequest(params operations.AddSignRuleParam Nickname: acc.Nickname, Description: params.Body.Description, SignRule: config.SignRule{ + Name: params.Body.Name, Contract: *params.Body.Contract, RuleType: config.RuleType(params.Body.RuleType), Enabled: *params.Body.Enabled, diff --git a/internal/handler/wallet/sign_rule_delete.go b/internal/handler/wallet/sign_rule_delete.go index ae6bda25e..0150758fc 100644 --- a/internal/handler/wallet/sign_rule_delete.go +++ b/internal/handler/wallet/sign_rule_delete.go @@ -30,9 +30,9 @@ func (w *deleteSignRuleHandler) Handle(params operations.DeleteSignRuleParams) m cfg := config.Get() - signRule, err := cfg.GetSignRule(acc.Nickname, params.RuleID) - if err != nil { - return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorDeleteSignRule, http.StatusInternalServerError) + signRule := cfg.GetSignRule(acc.Nickname, params.RuleID) + if signRule == nil { + return newErrorResponse(fmt.Sprintf("Rule ID %s not found", params.RuleID), errorDeleteSignRule, http.StatusInternalServerError) } promptRequest, err := w.getPromptRequest(acc, signRule) diff --git a/internal/handler/wallet/sign_rule_test.go b/internal/handler/wallet/sign_rule_test.go index d2ec49ab3..df3e4f5cb 100644 --- a/internal/handler/wallet/sign_rule_test.go +++ b/internal/handler/wallet/sign_rule_test.go @@ -34,8 +34,34 @@ func Test_signrule_Handlers(t *testing.T) { cfg := config.Get() + t.Run("Add sign rule with invalid contract", func(t *testing.T) { + contract := "Invalid Contract" + ruleType := config.RuleTypeAutoSign + ruleName := "Test Rule" + enabled := true + body := fmt.Sprintf(`{ + "name": "%s", + "contract": "%s", + "ruleType": "%s", + "enabled": %t, + "description": "Test Description" + }`, ruleName, contract, ruleType, enabled) + + resp, err := handleHTTPRequest(addhandler, "POST", fmt.Sprintf("/api/accounts/%s/signrules", nickname), body) + assert.NoError(err) + + verifyStatusCode(t, resp, http.StatusBadRequest) + + // check rule is not added + rulePtr := cfg.GetEnabledRuleForContract(account.Nickname, &contract) + assert.Nil(rulePtr) + + hasRule := cfg.HasEnabledRule(account.Nickname) + assert.False(hasRule) + }) + t.Run("Add sign rule", func(t *testing.T) { - contract := "TestAddContract" + contract := "AS12UMSUxgpRBB6ArZDJ19arHoxNkkpdfofQGekAiAJqsuE6PEFJy" ruleType := config.RuleTypeAutoSign ruleName := "Test Rule" enabled := true @@ -61,7 +87,6 @@ func Test_signrule_Handlers(t *testing.T) { resp, err := handleHTTPRequest(addhandler, "POST", fmt.Sprintf("/api/accounts/%s/signrules", nickname), body) assert.NoError(err) - fmt.Println(resp) verifyStatusCode(t, resp, http.StatusOK) @@ -83,8 +108,8 @@ func Test_signrule_Handlers(t *testing.T) { assert.True(hasRule) // check rule contains expected fields - rule, err := cfg.GetSignRule(account.Nickname, addRuleResponse.ID) - assert.NoError(err) + rule := cfg.GetSignRule(account.Nickname, addRuleResponse.ID) + assert.NotNil(rule) assert.Equal(rule.Name, ruleName) assert.Equal(rule.Contract, contract) assert.Equal(rule.RuleType, ruleType) @@ -162,8 +187,8 @@ func Test_signrule_Handlers(t *testing.T) { assert.NotEmpty(updateRuleResponse.ID) // Check rule is updated - rule, err := cfg.GetSignRule(account.Nickname, updateRuleResponse.ID) - assert.NoError(err) + rule := cfg.GetSignRule(account.Nickname, updateRuleResponse.ID) + assert.NotNil(rule) assert.Equal(rule.Name, updatedRuleName) assert.Equal(rule.Contract, contract) assert.Equal(rule.RuleType, ruleType) @@ -176,7 +201,7 @@ func Test_signrule_Handlers(t *testing.T) { }) t.Run("Delete sign rule", func(t *testing.T) { - contract := "TestDeleteContract" + contract := "AS1hCJXjndR4c9vekLWsXGnrdigp4AaZ7uYG3UKFzzKnWVsrNLPJ" ruleType := config.RuleTypeAutoSign ruleName := "Test Rule" enabled := true @@ -184,8 +209,7 @@ func Test_signrule_Handlers(t *testing.T) { "name": "%s", "contract": "%s", "ruleType": "%s", - "enabled": %t, - "description": "Test Description" + "enabled": %t }`, ruleName, contract, ruleType, enabled) testResult := make(chan walletapp.EventData) @@ -203,6 +227,7 @@ func Test_signrule_Handlers(t *testing.T) { // Add a sign rule first resp, err := handleHTTPRequest(addhandler, "POST", fmt.Sprintf("/api/accounts/%s/signrules", nickname), body) assert.NoError(err) + verifyStatusCode(t, resp, http.StatusOK) var addRuleResponse models.AddSignRuleResponse diff --git a/internal/handler/wallet/sign_rule_update.go b/internal/handler/wallet/sign_rule_update.go index 3f1a63a9f..295a3cf5d 100644 --- a/internal/handler/wallet/sign_rule_update.go +++ b/internal/handler/wallet/sign_rule_update.go @@ -30,14 +30,24 @@ func (w *updateSignRuleHandler) Handle(params operations.UpdateSignRuleParams) m return errResp } + newRule := config.SignRule{ + Name: params.Body.Name, + Contract: *params.Body.Contract, + RuleType: config.RuleType(params.Body.RuleType), + Enabled: *params.Body.Enabled, + } + cfg := config.Get() - signRule, err := cfg.GetSignRule(acc.Nickname, params.RuleID) - if err != nil { - return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorUpdateSignRule, http.StatusInternalServerError) + if signRule := cfg.GetSignRule(acc.Nickname, params.RuleID); signRule == nil { + return newErrorResponse(fmt.Sprintf("Rule ID %s not found", params.RuleID), errorUpdateSignRule, http.StatusInternalServerError) } - promptRequest, err := w.getPromptRequest(acc, signRule) + if err := config.ValidateRule(newRule); err != nil { + return operations.NewUpdateSignRuleBadRequest().WithPayload(&models.Error{Code: errorInvalidAssetAddress, Message: err.Error()}) + } + + promptRequest, err := w.getPromptRequest(acc, &newRule) if err != nil { return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorUpdateSignRule, http.StatusBadRequest) } @@ -52,13 +62,6 @@ func (w *updateSignRuleHandler) Handle(params operations.UpdateSignRuleParams) m return newErrorResponse(msg, errorGetWallets, http.StatusInternalServerError) } - newRule := config.SignRule{ - Name: params.Body.Name, - Contract: *params.Body.Contract, - RuleType: config.RuleType(params.Body.RuleType), - Enabled: *params.Body.Enabled, - } - ruleID, err := cfg.UpdateSignRule(acc.Nickname, params.RuleID, newRule) if err != nil { return newErrorResponse(fmt.Sprintf("Error: %v", err.Error()), errorUpdateSignRule, http.StatusInternalServerError) diff --git a/internal/handler/wallet/sign_test.go b/internal/handler/wallet/sign_test.go index 5d26fa681..bda815b62 100644 --- a/internal/handler/wallet/sign_test.go +++ b/internal/handler/wallet/sign_test.go @@ -233,7 +233,7 @@ func Test_walletSign_Handle(t *testing.T) { cfg := config.Get() ruleId, err := cfg.AddSignRule(nickname, config.SignRule{ Name: "test", - Contract: "AnotherContract", + Contract: "AS1ZGF1upwp9kPRvDKLxFAKRebgg7b3RWDnhgV7VvdZkZsUL7Nuv", RuleType: config.RuleTypeAutoSign, Enabled: true, }) @@ -362,7 +362,7 @@ func Test_walletSign_Handle(t *testing.T) { cfg := config.Get() ruleId, err := cfg.AddSignRule(nickname, config.SignRule{ Name: "test", - Contract: "DummyContract", + Contract: "AS12LKs9txoSSy8JgFJgV96m8k5z9pgzjYMYSshwN67mFVuj3bdUV", RuleType: config.RuleTypeDisablePasswordPrompt, Enabled: true, }) diff --git a/internal/handler/wallet/transfer.go b/internal/handler/wallet/transfer.go index 91f91e703..ab77f13f6 100644 --- a/internal/handler/wallet/transfer.go +++ b/internal/handler/wallet/transfer.go @@ -134,7 +134,7 @@ func doTransfer( ) (*sendOperation.OperationResponse, error) { operation, err := transaction.New(recipientAddress, amount) if err != nil { - return nil, fmt.Errorf("Error during transaction creation: %w", err) + return nil, fmt.Errorf("error during transaction creation: %w", err) } return network.SendOperation(acc, password, massaClient, operation, fee, chainID) diff --git a/pkg/app/enums.go b/pkg/app/enums.go new file mode 100644 index 000000000..5ffa67259 --- /dev/null +++ b/pkg/app/enums.go @@ -0,0 +1,51 @@ +package walletapp + +// Enum Bindings + +type EventType string + +const ( + PromptResultEvent string = "PROMPT_RESULT" + PromptDataEvent string = "PROMPT_DATA" + PromptRequestEvent string = "PROMPT_REQUEST" +) + +var EventTypes = []struct { + Value EventType + TSName string +}{ + {EventType(PromptResultEvent), "promptResult"}, + {EventType(PromptDataEvent), "promptData"}, + {EventType(PromptRequestEvent), "promptRequest"}, +} + +type PromptRequestAction string + +const ( + Delete PromptRequestAction = "DELETE_ACCOUNT" + NewPassword PromptRequestAction = "CREATE_PASSWORD" + Sign PromptRequestAction = "SIGN" + Import PromptRequestAction = "IMPORT_ACCOUNT" + Backup PromptRequestAction = "BACKUP_ACCOUNT" + TradeRolls PromptRequestAction = "TRADE_ROLLS" + Unprotect PromptRequestAction = "UNPROTECT" + AddSignRule PromptRequestAction = "ADD_SIGN_RULE" + DeleteSignRule PromptRequestAction = "DELETE_SIGN_RULE" + UpdateSignRule PromptRequestAction = "UPDATE_SIGN_RULE" +) + +var PromptRequest = []struct { + Value PromptRequestAction + TSName string +}{ + {Delete, "delete"}, + {NewPassword, "newPassword"}, + {Sign, "sign"}, + {Import, "import"}, + {Backup, "backup"}, + {TradeRolls, "tradeRolls"}, + {Unprotect, "unprotect"}, + {AddSignRule, "addSignRule"}, + {DeleteSignRule, "deleteSignRule"}, + {UpdateSignRule, "updateSignRule"}, +} diff --git a/pkg/app/events.go b/pkg/app/events.go index 5b9c02e07..2cdf8a878 100644 --- a/pkg/app/events.go +++ b/pkg/app/events.go @@ -2,12 +2,6 @@ package walletapp import "github.com/awnumar/memguard" -const ( - PromptResultEvent string = "promptResult" - PromptDataEvent string = "promptData" - PromptRequestEvent string = "promptRequest" -) - // Events type EventData struct { @@ -22,21 +16,6 @@ const ( Cancel PromptCtrl = iota ) -type PromptRequestAction int - -const ( - Delete PromptRequestAction = iota - NewPassword - Sign - Import - Backup - TradeRolls - Unprotect - AddSignRule - DeleteSignRule - UpdateSignRule -) - // User input interfaces for the channel // EventInterface interface that all message types will implement diff --git a/pkg/config/config.go b/pkg/config/config.go index b3ebe8eda..a193f75fb 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -128,7 +128,7 @@ func SaveConfig(config *Config) error { func saveConfigUnsafe(config *Config) error { // Check for duplicate IDs if err := config.ensureUniqueRuleIDs(); err != nil { - return fmt.Errorf("duplicate SignRule IDs found: %v", err) + return err } path := configFilePath() diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 1e509d9f1..a5af7d6d5 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -1,7 +1,6 @@ package config import ( - "fmt" "log" "os" "testing" @@ -30,7 +29,7 @@ func TestMain(m *testing.M) { func TestAddSignRule(t *testing.T) { rule := SignRule{ Name: "Test Rule", - Contract: "test_contract", + Contract: "AS12U4TZfNK7qoLyEERBBRDMu8nm5MKoRzPXDXans4v9wdATZedz9", RuleType: RuleTypeAutoSign, Enabled: true, } @@ -48,7 +47,7 @@ func TestAddSignRule(t *testing.T) { // Add another rule rule2 := SignRule{ Name: "Test Rule 2", - Contract: "test_contract_2", + Contract: "AS133eqPPaPttJ6hJnk3sfoG5cjFFqBDi1VGxdo2wzWkq8AfZnan", RuleType: RuleTypeDisablePasswordPrompt, Enabled: false, } @@ -76,7 +75,7 @@ func TestAddSignRule(t *testing.T) { } func TestDeleteSignRule(t *testing.T) { - contract := "test_delete_contract" + contract := "AS12UMSUxgpRBB6ArZDJ19arHoxNkkpdfofQGekAiAJqsuE6PEFJy" rule := SignRule{ Name: "Test Rule", @@ -85,8 +84,6 @@ func TestDeleteSignRule(t *testing.T) { Enabled: true, } - fmt.Println("TestDeleteSignRule: adding rule", rule) - ruleID, err := cfg.AddSignRule(accountName, rule) assert.NoError(t, err) @@ -94,14 +91,13 @@ func TestDeleteSignRule(t *testing.T) { assert.NoError(t, err) // Verify the rule was deleted - deletedRule, err := cfg.GetSignRule(accountName, ruleID) - assert.Error(t, err, "rule not found") + deletedRule := cfg.GetSignRule(accountName, ruleID) assert.Nil(t, deletedRule) } func TestUpdateSignRule(t *testing.T) { accountName := "test_account" - contract := "test_update_contract" + contract := "AS12UMSUxgpRBB6ArZDJ19arHoxNkkpdfofQGekAiAJqsuE6PEFJy" rule := SignRule{ Name: "Test Rule", Contract: contract, @@ -114,7 +110,7 @@ func TestUpdateSignRule(t *testing.T) { newRule := SignRule{ Name: "Updated Rule", - Contract: "updated_contract", + Contract: "AS125oPLYRTtfVjpWisPZVTLjBhCFfQ1jDsi75XNtRm1NZux54eCj", RuleType: RuleTypeDisablePasswordPrompt, Enabled: false, } @@ -124,8 +120,8 @@ func TestUpdateSignRule(t *testing.T) { assert.NotEqual(t, ruleID, newRuleID) // Verify the rule was updated - updatedRule, err := cfg.GetSignRule(accountName, newRuleID) - assert.NoError(t, err) + updatedRule := cfg.GetSignRule(accountName, newRuleID) + assert.NotNil(t, updatedRule) assert.Equal(t, newRuleID, updatedRule.ID) assert.Equal(t, newRule.Name, updatedRule.Name) assert.Equal(t, newRule.Contract, updatedRule.Contract) @@ -133,15 +129,14 @@ func TestUpdateSignRule(t *testing.T) { assert.Equal(t, newRule.Enabled, updatedRule.Enabled) // Verify previous rule is deleted - deletedRule, err := cfg.GetSignRule(accountName, ruleID) - assert.Error(t, err, "rule not found") + deletedRule := cfg.GetSignRule(accountName, ruleID) assert.Nil(t, deletedRule) } func TestValidateRuleID(t *testing.T) { rule := SignRule{ Name: "Test Rule", - Contract: "test_contract", + Contract: "AS1hCJXjndR4c9vekLWsXGnrdigp4AaZ7uYG3UKFzzKnWVsrNLPJ", RuleType: RuleTypeAutoSign, Enabled: true, } @@ -158,7 +153,7 @@ func TestValidateRuleID(t *testing.T) { func TestHasEnabledRule(t *testing.T) { accountName := "test_account" - contract := "test_hasEnable_contract" + contract := "AS124vf3YfAJCSCQVYKczzuWWpXrximFpbTmX4rheLs5uNSftiiRY" rule := SignRule{ Name: "Test Rule", Contract: contract, diff --git a/pkg/config/rules.go b/pkg/config/rules.go index 140921c02..7d3324b0c 100644 --- a/pkg/config/rules.go +++ b/pkg/config/rules.go @@ -4,10 +4,12 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + + "github.com/massalabs/station-massa-wallet/pkg/utils" ) func (c *Config) AddSignRule(accountName string, rule SignRule) (string, error) { - if err := validateRule(rule); err != nil { + if err := ValidateRule(rule); err != nil { return "", fmt.Errorf("invalid rule: %v", err) } @@ -24,8 +26,8 @@ func (c *Config) AddSignRule(accountName string, rule SignRule) (string, error) } // check if the rule already exists - _, err := c.getSignRuleUnsafe(accountName, rule.ID) - if err == nil { + existingRule := c.getSignRuleUnsafe(accountName, rule.ID) + if existingRule != nil { return "", fmt.Errorf("rule %s already exists", rule.ID) } @@ -33,8 +35,7 @@ func (c *Config) AddSignRule(accountName string, rule SignRule) (string, error) account.SignRules = append(account.SignRules, rule) c.Accounts[accountName] = account - err = saveConfigUnsafe(c) - if err != nil { + if err := saveConfigUnsafe(c); err != nil { return "", fmt.Errorf("error saving configuration: %v", err) } @@ -79,6 +80,10 @@ func (c *Config) DeleteSignRule(accountName, ruleID string) error { } func (c *Config) UpdateSignRule(accountName, ruleID string, newRule SignRule) (string, error) { + if err := ValidateRule(newRule); err != nil { + return "", fmt.Errorf("invalid rule: %v", err) + } + configManager.mu.Lock() defer configManager.mu.Unlock() @@ -110,7 +115,11 @@ func (c *Config) UpdateSignRule(accountName, ruleID string, newRule SignRule) (s return newRule.ID, nil } -func validateRule(rule SignRule) error { +func ValidateRule(rule SignRule) error { + if rule.Contract != "*" && !utils.IsValidContract(rule.Contract) { + return fmt.Errorf("invalid contract address: %s", rule.Contract) + } + switch rule.RuleType { case RuleTypeDisablePasswordPrompt: return nil @@ -178,24 +187,24 @@ func (c *Config) validateAllRuleIDs() error { return nil } -func (c *Config) getSignRuleUnsafe(accountName, ruleID string) (*SignRule, error) { +func (c *Config) getSignRuleUnsafe(accountName, ruleID string) *SignRule { // Check if the account exists account, exists := c.Accounts[accountName] if !exists { - return nil, fmt.Errorf("account not found: %s", accountName) + return nil } // Search for the rule by ID for _, rule := range account.SignRules { if rule.ID == ruleID { - return &rule, nil + return &rule } } - return nil, fmt.Errorf("rule not found: %s", ruleID) + return nil } -func (c *Config) GetSignRule(accountName, ruleID string) (*SignRule, error) { +func (c *Config) GetSignRule(accountName, ruleID string) *SignRule { configManager.mu.RLock() defer configManager.mu.RUnlock() @@ -256,3 +265,15 @@ func (c *Config) GetEnabledRuleForContract(accountName string, contract *string) return ruleType } + +func (c *Config) IsExistingRule(accountName string, rule SignRule) bool { + configManager.mu.RLock() + defer configManager.mu.RUnlock() + + // Generate the expected ID based on the rule's parameters + expectedID := generateRuleID(accountName, rule) + + rulePtr := c.getSignRuleUnsafe(accountName, expectedID) + + return rulePtr != nil +} diff --git a/pkg/utils/address.go b/pkg/utils/address.go index de9b0e938..398fb0280 100644 --- a/pkg/utils/address.go +++ b/pkg/utils/address.go @@ -2,21 +2,23 @@ package utils import "github.com/massalabs/station/pkg/node/base58" -// IsValidAddress checks if the address is valid based on the prefix rule, non-empty rule, and successful decoding. func IsValidAddress(addr string) bool { - if addr == "" { + if len(addr) <= 2 { return false } - addressPrefix := addr[:2] addressWithoutPrefix := addr[2:] - if addressPrefix == "AS" && len(addressWithoutPrefix) > 0 { - _, _, err := base58.VersionedCheckDecode(addressWithoutPrefix) - if err == nil { - return true - } + _, _, err := base58.VersionedCheckDecode(addressWithoutPrefix) + + return err == nil +} + +func IsValidContract(addr string) bool { + if !IsValidAddress(addr) { + return false } + addressPrefix := addr[:2] - return false + return addressPrefix == "AS" } diff --git a/pkg/wails/wails.go b/pkg/wails/wails.go index d44930b0a..88a3bc198 100644 --- a/pkg/wails/wails.go +++ b/pkg/wails/wails.go @@ -24,5 +24,9 @@ func NewWailsApp(app *wApp.WalletApp, assets embed.FS) *application.Application Bind: []interface{}{ app, }, + EnumBind: []interface{}{ + wApp.PromptRequest, + wApp.EventTypes, + }, }) } diff --git a/wails-frontend/package-lock.json b/wails-frontend/package-lock.json index b7a8c9747..4a8d63640 100644 --- a/wails-frontend/package-lock.json +++ b/wails-frontend/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "@massalabs/massa-web3": "^5.1.0", - "@massalabs/react-ui-kit": "^1.0.2", + "@massalabs/react-ui-kit": "^1.0.3-dev.20250122150606", "currency.js": "^2.0.4", "dot-object": "^2.1.4", "esbuild": "^0.17.19", @@ -2032,9 +2032,9 @@ } }, "node_modules/@massalabs/react-ui-kit": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@massalabs/react-ui-kit/-/react-ui-kit-1.0.2.tgz", - "integrity": "sha512-+HzUCUBaOdJrYoTvt3xL/heW1OVOvrnHVclzPZPk8SZ27cZEbncJeq3q+LB0KFlEb4r7TUVp72osxkBfltqHAQ==", + "version": "1.0.3-dev.20250122150606", + "resolved": "https://registry.npmjs.org/@massalabs/react-ui-kit/-/react-ui-kit-1.0.3-dev.20250122150606.tgz", + "integrity": "sha512-uBNoNtfU8zEAwyb3g0K/g3SpZHOwNw6WprBDH2maGyQh6+DRId/PmlHBYeISGR9uxiBEbNnMTnNNnv7dk+KJ0Q==", "dependencies": { "@headlessui/react": "^1.7.15", "@massalabs/massa-web3": "^5.1.1", diff --git a/wails-frontend/package.json b/wails-frontend/package.json index bfa4f94bc..6883624f8 100644 --- a/wails-frontend/package.json +++ b/wails-frontend/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "@massalabs/massa-web3": "^5.1.0", - "@massalabs/react-ui-kit": "^1.0.2", + "@massalabs/react-ui-kit": "^1.0.3-dev.20250122150606", "currency.js": "^2.0.4", "dot-object": "^2.1.4", "esbuild": "^0.17.19", diff --git a/wails-frontend/package.json.md5 b/wails-frontend/package.json.md5 index 76fdba50e..316d51841 100755 --- a/wails-frontend/package.json.md5 +++ b/wails-frontend/package.json.md5 @@ -1 +1 @@ -01bf439a1eae333a806df63eb95be49c \ No newline at end of file +c65c84ad1d9819f5c36e51ecb5a2c005 \ No newline at end of file diff --git a/wails-frontend/src/app.tsx b/wails-frontend/src/app.tsx index 6e73c39a6..45c7c9834 100644 --- a/wails-frontend/src/app.tsx +++ b/wails-frontend/src/app.tsx @@ -1,44 +1,45 @@ import { EventsOn } from '@wailsjs/runtime/runtime'; import { useNavigate } from 'react-router-dom'; -import { - events, - promptAction, - promptRequest, - promptResult, -} from './events/events'; +import { promptRequest, promptResult } from './events'; import Intl from './i18n/i18n'; import { Loading } from './pages/loading'; import { useConfigStore } from './store/store'; import { ErrorCode } from './utils'; +import { walletapp } from '../wailsjs/go/models'; export function App() { const navigate = useNavigate(); + const { PromptRequestAction, EventType } = walletapp; + const handlePromptRequest = (req: promptRequest) => { clearTimeout(useConfigStore.getState().timeoutId); switch (req.Action) { - case promptAction.deleteReq: - case promptAction.signReq: - case promptAction.tradeRollsReq: - case promptAction.unprotectReq: + case PromptRequestAction.delete: + case PromptRequestAction.sign: + case PromptRequestAction.tradeRolls: + case PromptRequestAction.unprotect: + case PromptRequestAction.addSignRule: + case PromptRequestAction.deleteSignRule: + case PromptRequestAction.updateSignRule: navigate('/password', { state: { req } }); return; - case promptAction.newPasswordReq: + case PromptRequestAction.newPassword: navigate('/new-password', { state: { req } }); return; - case promptAction.importReq: + case PromptRequestAction.import: navigate('/import-methods', { state: { req } }); return; - case promptAction.backupReq: + case PromptRequestAction.backup: navigate('/backup-methods', { state: { req } }); return; default: } }; - EventsOn(events.promptResult, (result: promptResult) => { + EventsOn(EventType.promptResult, (result: promptResult) => { if (!result.Success && result.CodeMessage === ErrorCode.Timeout) { const errorMessage = Intl.t(`errors.${result.CodeMessage}`); navigate('/failure', { @@ -47,7 +48,7 @@ export function App() { } }); - EventsOn(events.promptRequest, handlePromptRequest); + EventsOn(EventType.promptRequest, handlePromptRequest); return ; } diff --git a/wails-frontend/src/events/index.ts b/wails-frontend/src/events/index.ts new file mode 100644 index 000000000..fcb073fef --- /dev/null +++ b/wails-frontend/src/events/index.ts @@ -0,0 +1 @@ +export * from './types'; diff --git a/wails-frontend/src/events/events.tsx b/wails-frontend/src/events/types.tsx similarity index 52% rename from wails-frontend/src/events/events.tsx rename to wails-frontend/src/events/types.tsx index 6d6b33eb9..f80b45c8b 100644 --- a/wails-frontend/src/events/events.tsx +++ b/wails-frontend/src/events/types.tsx @@ -1,3 +1,5 @@ +import { walletapp } from '@wailsjs/go/models'; + /* eslint-disable @typescript-eslint/no-explicit-any */ export type promptResult = { Success: boolean; @@ -5,29 +7,13 @@ export type promptResult = { Data: T; }; -export enum promptAction { - deleteReq = 0, - newPasswordReq = 1, - signReq = 2, - importReq = 3, - backupReq = 4, - tradeRollsReq = 6, - unprotectReq = 7, -} - export type promptRequest = { - Action: promptAction; + Action: walletapp.PromptRequestAction; Msg: string; Data: any; CodeMessage: string; }; -export const events = { - promptResult: 'promptResult', - promptData: 'promptData', - promptRequest: 'promptRequest', -}; - export const backupMethods = { ymlFileBackup: 'yaml', privateKeyBackup: 'privateKey', diff --git a/wails-frontend/src/i18n/en_US.json b/wails-frontend/src/i18n/en_US.json index 62ef24f26..6333bc3d9 100644 --- a/wails-frontend/src/i18n/en_US.json +++ b/wails-frontend/src/i18n/en_US.json @@ -14,7 +14,33 @@ "password": "Password" } }, + "signRule": { + "addRule": { + "title": "Add a signature rule" + }, + "updateRule": { + "title": "Update a signature rule" + }, + "deleteRule": { + "title": "Delete a signature rule" + }, + "behavior": { + "disable_password_prompt": "You are allowing the wallet to remember your password and enable one click sign for the selected contract(s)", + "auto_sign": "You are allowing the wallet to sign transactions without asking for confirmation for the selected contract" + }, + "description": "Description from the requesting dApp", + "details": "Rule details", + "contract": "Contract", + "name": "Name", + "ruleType": "Rule type", + "enabled": "Enable", + "rulesTypeDesc": { + "auto_sign": "Auto sign", + "disable_password_prompt": "Remember password" + } + }, "password-prompt": { + "account": "Account:", "transfer": { "from": "from: ", "to": "to: ", @@ -160,7 +186,11 @@ "Wallet-0012": "Failed to sign: invalid operation", "Wallet-0021": "Failed to sign", "transaction": "Failed to process transaction", - "delete": "Failed to process delete operation", - "default": "Failed to process operation" + "delete": "Failed to delete account", + "default": "Failed to process operation", + "addRule": "Failed to add signature rule", + "updateRule": "Failed to update signature rule", + "deleteRule": "Failed to delete signature rule", + "unknownRuleRequest": "Unknown signature rule request" } } diff --git a/wails-frontend/src/pages/ImportPrivateKey/PromptNickname.tsx b/wails-frontend/src/pages/ImportPrivateKey/PromptNickname.tsx index 68aab3d3a..76c87fc3c 100644 --- a/wails-frontend/src/pages/ImportPrivateKey/PromptNickname.tsx +++ b/wails-frontend/src/pages/ImportPrivateKey/PromptNickname.tsx @@ -8,7 +8,7 @@ import { import { useLocation, useNavigate } from 'react-router-dom'; import { IMPORT_STEPS } from '@/const/stepper'; -import { promptRequest } from '@/events/events'; +import { promptRequest } from '@/events'; import Intl from '@/i18n/i18n'; import { Layout } from '@/layouts/Layout/Layout'; import { IErrorObject, parseForm, handleCancel } from '@/utils'; diff --git a/wails-frontend/src/pages/ImportPrivateKey/PromptPrivateKey.tsx b/wails-frontend/src/pages/ImportPrivateKey/PromptPrivateKey.tsx index e0f86436e..ed9b5af8c 100644 --- a/wails-frontend/src/pages/ImportPrivateKey/PromptPrivateKey.tsx +++ b/wails-frontend/src/pages/ImportPrivateKey/PromptPrivateKey.tsx @@ -4,7 +4,7 @@ import { Password, Button, Stepper } from '@massalabs/react-ui-kit'; import { useLocation, useNavigate } from 'react-router-dom'; import { IMPORT_STEPS } from '@/const/stepper'; -import { promptRequest } from '@/events/events'; +import { promptRequest } from '@/events'; import Intl from '@/i18n/i18n'; import { Layout } from '@/layouts/Layout/Layout'; import { IErrorObject, parseForm, handleCancel } from '@/utils'; diff --git a/wails-frontend/src/pages/PasswordPromptHandler/BuySellRoll/BuySellRoll.tsx b/wails-frontend/src/pages/PasswordPromptHandler/BuySellRoll/BuySellRoll.tsx index bb743134c..a4cab467f 100644 --- a/wails-frontend/src/pages/PasswordPromptHandler/BuySellRoll/BuySellRoll.tsx +++ b/wails-frontend/src/pages/PasswordPromptHandler/BuySellRoll/BuySellRoll.tsx @@ -1,5 +1,4 @@ -import { formatAmount } from '@massalabs/react-ui-kit'; -import { massaToken } from '@massalabs/react-ui-kit/src/lib/massa-react/utils/const'; +import { formatAmount, massaToken } from '@massalabs/react-ui-kit'; import { OPER_BUY_ROLL } from '@/const/operations'; import Intl from '@/i18n/i18n'; diff --git a/wails-frontend/src/pages/PasswordPromptHandler/CallSC/FTTransferInfo.tsx b/wails-frontend/src/pages/PasswordPromptHandler/CallSC/FTTransferInfo.tsx index 10dc9f984..2ed098e58 100644 --- a/wails-frontend/src/pages/PasswordPromptHandler/CallSC/FTTransferInfo.tsx +++ b/wails-frontend/src/pages/PasswordPromptHandler/CallSC/FTTransferInfo.tsx @@ -1,6 +1,5 @@ import { Args } from '@massalabs/massa-web3'; -import { formatAmount } from '@massalabs/react-ui-kit'; -import { maskAddress } from '@massalabs/react-ui-kit/src/lib/massa-react/utils'; +import { formatAmount, maskAddress } from '@massalabs/react-ui-kit'; import { LogPrint } from '@wailsjs/runtime/runtime'; import Intl from '@/i18n/i18n'; diff --git a/wails-frontend/src/pages/PasswordPromptHandler/Delete.tsx b/wails-frontend/src/pages/PasswordPromptHandler/Delete.tsx index 7b2cf8952..168472849 100644 --- a/wails-frontend/src/pages/PasswordPromptHandler/Delete.tsx +++ b/wails-frontend/src/pages/PasswordPromptHandler/Delete.tsx @@ -1,12 +1,13 @@ import { SyntheticEvent, useRef, useState } from 'react'; import { Button, Password } from '@massalabs/react-ui-kit'; +import { walletapp } from '@wailsjs/go/models'; import { SendPromptInput } from '@wailsjs/go/walletapp/WalletApp'; import { EventsOnce } from '@wailsjs/runtime/runtime'; import { FiTrash2 } from 'react-icons/fi'; import { useLocation, useNavigate } from 'react-router-dom'; -import { events, promptRequest, promptResult } from '@/events/events'; +import { promptRequest, promptResult } from '@/events'; import Intl from '@/i18n/i18n'; import { Layout } from '@/layouts/Layout/Layout'; import { PromptRequestDeleteData } from '@/pages/PasswordPromptHandler/PasswordPromptHandler'; @@ -29,11 +30,13 @@ export function Delete() { const req: promptRequest = state.req; const data: PromptRequestDeleteData = req.Data; + const { EventType } = walletapp; + function save(e: SyntheticEvent) { const form = parseForm(e); const { password } = form; - EventsOnce(events.promptResult, handleResult); + EventsOnce(EventType.promptResult, handleResult); SendPromptInput(password); } diff --git a/wails-frontend/src/pages/PasswordPromptHandler/ExecuteSC.tsx/ExecuteSc.tsx b/wails-frontend/src/pages/PasswordPromptHandler/ExecuteSC.tsx/ExecuteSc.tsx index 421c7e44e..752a95f63 100644 --- a/wails-frontend/src/pages/PasswordPromptHandler/ExecuteSC.tsx/ExecuteSc.tsx +++ b/wails-frontend/src/pages/PasswordPromptHandler/ExecuteSC.tsx/ExecuteSc.tsx @@ -1,5 +1,4 @@ -import { Tooltip, formatAmount } from '@massalabs/react-ui-kit'; -import { massaToken } from '@massalabs/react-ui-kit/src/lib/massa-react/utils/const'; +import { Tooltip, formatAmount, massaToken } from '@massalabs/react-ui-kit'; import { FiAlertTriangle, FiInfo } from 'react-icons/fi'; import Intl from '@/i18n/i18n'; diff --git a/wails-frontend/src/pages/PasswordPromptHandler/PasswordPromptHandler.tsx b/wails-frontend/src/pages/PasswordPromptHandler/PasswordPromptHandler.tsx index 1adb5dd69..5d84fc20a 100644 --- a/wails-frontend/src/pages/PasswordPromptHandler/PasswordPromptHandler.tsx +++ b/wails-frontend/src/pages/PasswordPromptHandler/PasswordPromptHandler.tsx @@ -1,8 +1,10 @@ +import { walletapp } from '@wailsjs/go/models'; import { useLocation } from 'react-router-dom'; import { Delete } from './Delete'; import { Sign } from './Sign'; -import { promptAction, promptRequest } from '@/events/events'; +import { SignRule } from '../SignRuleHandler/signRulePrompt'; +import { promptRequest } from '@/events'; export interface PromptRequestDeleteData { Nickname: string; @@ -12,16 +14,21 @@ export interface PromptRequestDeleteData { export default function PasswordPromptHandler() { const { state } = useLocation(); const req: promptRequest = state.req; - const { deleteReq, signReq } = promptAction; + + const { PromptRequestAction } = walletapp; return ( <> {(() => { switch (req.Action) { - case deleteReq: + case PromptRequestAction.delete: return ; - case signReq: + case PromptRequestAction.sign: return ; + case PromptRequestAction.addSignRule: + case PromptRequestAction.updateSignRule: + case PromptRequestAction.deleteSignRule: + return ; } })()} diff --git a/wails-frontend/src/pages/PasswordPromptHandler/Sign.tsx b/wails-frontend/src/pages/PasswordPromptHandler/Sign.tsx index 00fe2d704..ea96b5d3c 100644 --- a/wails-frontend/src/pages/PasswordPromptHandler/Sign.tsx +++ b/wails-frontend/src/pages/PasswordPromptHandler/Sign.tsx @@ -2,6 +2,7 @@ import { SyntheticEvent, useRef, useState } from 'react'; import { Mas } from '@massalabs/massa-web3'; import { Button, Password } from '@massalabs/react-ui-kit'; +import { walletapp } from '@wailsjs/go/models'; import { SendSignPromptInput } from '@wailsjs/go/walletapp/WalletApp'; import { EventsOnce, WindowSetSize } from '@wailsjs/runtime/runtime'; import { FiLock } from 'react-icons/fi'; @@ -22,7 +23,7 @@ import { OPER_SELL_ROLL, OPER_TRANSACTION, } from '@/const/operations'; -import { events, promptRequest, promptResult } from '@/events/events'; +import { promptRequest, promptResult } from '@/events'; import Intl from '@/i18n/i18n'; import { SignLayout } from '@/layouts/Layout/SignLayout'; import { @@ -92,11 +93,13 @@ export function Sign() { const [fees, setFees] = useState(BigInt(signData.Fees || 0)); const [isEditing, setIsEditing] = useState(false); + const { EventType } = walletapp; + function save(e: SyntheticEvent) { const form = parseForm(e); const { password } = form; - EventsOnce(events.promptResult, handleResult); + EventsOnce(EventType.promptResult, handleResult); SendSignPromptInput(password, fees.toString()); } diff --git a/wails-frontend/src/pages/PasswordPromptHandler/SignComponentUtils/From.tsx b/wails-frontend/src/pages/PasswordPromptHandler/SignComponentUtils/From.tsx index 4cf84a293..79431ea25 100644 --- a/wails-frontend/src/pages/PasswordPromptHandler/SignComponentUtils/From.tsx +++ b/wails-frontend/src/pages/PasswordPromptHandler/SignComponentUtils/From.tsx @@ -1,7 +1,4 @@ -import { - maskAddress, - maskNickname, -} from '@massalabs/react-ui-kit/src/lib/massa-react/utils'; +import { maskAddress, maskNickname } from '@massalabs/react-ui-kit'; import Intl from '@/i18n/i18n'; diff --git a/wails-frontend/src/pages/PasswordPromptHandler/SignComponentUtils/FromTo.tsx b/wails-frontend/src/pages/PasswordPromptHandler/SignComponentUtils/FromTo.tsx index 501b44b4a..f6fe24b4f 100644 --- a/wails-frontend/src/pages/PasswordPromptHandler/SignComponentUtils/FromTo.tsx +++ b/wails-frontend/src/pages/PasswordPromptHandler/SignComponentUtils/FromTo.tsx @@ -1,9 +1,7 @@ -import { - maskAddress, - maskNickname, -} from '@massalabs/react-ui-kit/src/lib/massa-react/utils'; +import { maskAddress, maskNickname } from '@massalabs/react-ui-kit'; import { FiArrowRight } from 'react-icons/fi'; +import { CopyClip } from '../components/clipBoardCopy'; import Intl from '@/i18n/i18n'; interface FromToProps { @@ -46,7 +44,12 @@ export function FromTo(props: FromToProps) {

) : null} -

{maskAddress(recipientAddress)}

+
+

{maskAddress(recipientAddress)}

+
+ +
+
); diff --git a/wails-frontend/src/pages/PasswordPromptHandler/SignComponentUtils/OperationCost.tsx b/wails-frontend/src/pages/PasswordPromptHandler/SignComponentUtils/OperationCost.tsx index 878531057..94ca9e21c 100644 --- a/wails-frontend/src/pages/PasswordPromptHandler/SignComponentUtils/OperationCost.tsx +++ b/wails-frontend/src/pages/PasswordPromptHandler/SignComponentUtils/OperationCost.tsx @@ -13,8 +13,8 @@ import { InlineMoney, Tooltip, formatAmount, + massaToken, } from '@massalabs/react-ui-kit'; -import { massaToken } from '@massalabs/react-ui-kit/src/lib/massa-react/utils/const'; import { FiCheck, FiChevronDown, diff --git a/wails-frontend/src/pages/PasswordPromptHandler/Transaction/Transaction.tsx b/wails-frontend/src/pages/PasswordPromptHandler/Transaction/Transaction.tsx index 425a392c8..7e367e2e4 100644 --- a/wails-frontend/src/pages/PasswordPromptHandler/Transaction/Transaction.tsx +++ b/wails-frontend/src/pages/PasswordPromptHandler/Transaction/Transaction.tsx @@ -1,5 +1,4 @@ -import { formatAmount } from '@massalabs/react-ui-kit'; -import { massaToken } from '@massalabs/react-ui-kit/src/lib/massa-react/utils/const'; +import { formatAmount, massaToken } from '@massalabs/react-ui-kit'; import Intl from '@/i18n/i18n'; import { AmountBox } from '@/pages/PasswordPromptHandler/AmountBox'; diff --git a/wails-frontend/src/pages/PasswordPromptHandler/components/account.tsx b/wails-frontend/src/pages/PasswordPromptHandler/components/account.tsx new file mode 100644 index 000000000..9787a4a94 --- /dev/null +++ b/wails-frontend/src/pages/PasswordPromptHandler/components/account.tsx @@ -0,0 +1,28 @@ +import { maskAddress, maskNickname } from '@massalabs/react-ui-kit'; + +import { CopyClip } from './clipBoardCopy'; +import Intl from '@/i18n/i18n'; + +interface FromProps { + nickname: string; + walletAddress: string; +} + +export function Account(props: FromProps) { + const { nickname, walletAddress } = props; + + return ( +
+

{Intl.t('password-prompt.account')}

+
+

{maskNickname(nickname)}

+
+

{maskAddress(walletAddress)}

+
+ +
+
+
+
+ ); +} diff --git a/wails-frontend/src/pages/PasswordPromptHandler/components/clipBoardCopy.tsx b/wails-frontend/src/pages/PasswordPromptHandler/components/clipBoardCopy.tsx new file mode 100644 index 000000000..ada50177b --- /dev/null +++ b/wails-frontend/src/pages/PasswordPromptHandler/components/clipBoardCopy.tsx @@ -0,0 +1,14 @@ +import { ClipboardSetText } from '@wailsjs/runtime'; +import { FiCopy } from 'react-icons/fi'; + +interface CopyProps { + data: string; +} + +export function CopyClip({ data }: CopyProps) { + async function handleCopy() { + await ClipboardSetText(data); + } + + return ; +} diff --git a/wails-frontend/src/pages/SignRuleHandler/signRulePrompt.tsx b/wails-frontend/src/pages/SignRuleHandler/signRulePrompt.tsx new file mode 100644 index 000000000..75e4ec3b8 --- /dev/null +++ b/wails-frontend/src/pages/SignRuleHandler/signRulePrompt.tsx @@ -0,0 +1,180 @@ +import { SyntheticEvent, useRef, useState } from 'react'; + +import { Button, Password, maskAddress } from '@massalabs/react-ui-kit'; +import { walletapp } from '@wailsjs/go/models'; +import { SendPromptInput } from '@wailsjs/go/walletapp/WalletApp'; +import { EventsOnce, WindowSetSize } from '@wailsjs/runtime/runtime'; +import { FiAlertTriangle } from 'react-icons/fi'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { ruleRequestData, signRuleActionStr } from './types'; +import { Account } from '../PasswordPromptHandler/components/account'; +import { CopyClip } from '../PasswordPromptHandler/components/clipBoardCopy'; +import { promptRequest, promptResult } from '@/events'; +import Intl from '@/i18n/i18n'; +import { Layout } from '@/layouts/Layout/Layout'; +import { validate } from '@/pages/PasswordPromptHandler/Sign'; +import { + ErrorCode, + IErrorObject, + handleApplyResult, + handleCancel, + parseForm, +} from '@/utils'; + +export function SignRule() { + const [error, setError] = useState(null); + const [errorMessage, setErrorMessage] = useState(''); + + const navigate = useNavigate(); + const form = useRef(null); + const { state } = useLocation(); + const req: promptRequest = state.req; + const data: ruleRequestData = req.Data; + + let action = ''; + let showWarning = false; + + const { addSignRule, updateSignRule, deleteSignRule } = + walletapp.PromptRequestAction; + + const { EventType } = walletapp; + + switch (req.Action) { + case addSignRule: + action = signRuleActionStr.addSignRule; + showWarning = data.SignRule.Enabled; + break; + case updateSignRule: + action = signRuleActionStr.updateSignRule; + showWarning = data.SignRule.Enabled; + break; + case deleteSignRule: + action = signRuleActionStr.deleteSignRule; + break; + default: + setErrorMessage(Intl.t(`errors.unknownRuleRequest`)); + } + + const winWidth = 460; + const winHeight = showWarning ? 650 : 540; + + WindowSetSize(winWidth, winHeight); + + function handleResult(result: promptResult) { + let { Success, CodeMessage } = result; + setError(null); + setErrorMessage(''); + + if (!Success) { + if (CodeMessage === ErrorCode.WrongPassword) { + setError({ password: Intl.t(`errors.${CodeMessage}`) }); + } else { + setErrorMessage(Intl.t(`errors.${action}`)); + } + return; + } + handleApplyResult(navigate, req, setError, false)(result); + } + + function submitPassword(e: SyntheticEvent) { + const form = parseForm(e); + const { password } = form; + + EventsOnce(EventType.promptResult, handleResult); + + SendPromptInput(password); + } + + async function handleSubmitPassword(e: SyntheticEvent) { + e.preventDefault(); + if (!validate(e, setError)) return; + submitPassword(e); + } + + const isAllContract = data.SignRule.Contract === '*'; + + const renderContract = () => { + if (isAllContract) { + return 'All'; + } + return maskAddress(data.SignRule.Contract, 10); + }; + + return ( + +
+

{Intl.t(`signRule.${action}.title`)}

+ {data.Description && ( + <> +
+ {Intl.t('signRule.description')}: +
+
+ {data.Description} +
+ + )} + + + + {showWarning && ( +
+ +
+

{Intl.t(`signRule.behavior.${data.SignRule.RuleType}`)}

+
+
+ )} + +
+ {/*
+

{Intl.t('signRule.details')}:

+
*/} +

+ {Intl.t('signRule.name')}: {data.SignRule.Name} +

+
+

+ {Intl.t('signRule.contract')}: {renderContract()} +

+ {!isAllContract && ( +
+ +
+ )} +
+ +

+ {Intl.t('signRule.ruleType')}:{' '} + {Intl.t(`signRule.rulesTypeDesc.${data.SignRule.RuleType}`)} +

+

+ {Intl.t('signRule.enabled')}:{' '} + {data.SignRule.Enabled ? 'Yes' : 'No'} +

+
+ +
+ + {errorMessage && ( +

{errorMessage}

+ )} +
+
+ + +
+ +
+ ); +} diff --git a/wails-frontend/src/pages/SignRuleHandler/types.tsx b/wails-frontend/src/pages/SignRuleHandler/types.tsx new file mode 100644 index 000000000..e88dc7474 --- /dev/null +++ b/wails-frontend/src/pages/SignRuleHandler/types.tsx @@ -0,0 +1,24 @@ +export enum RuleType { + disablePasswordPrompt = 'disable_password_prompt', + autoSign = 'auto_sign', +} + +// Used in i18n +export enum signRuleActionStr { + addSignRule = 'addRule', + deleteSignRule = 'deleteRule', + updateSignRule = 'updateRule', +} + +export interface ruleRequestData { + WalletAddress: string; + Nickname: string; + Description: string; + SignRule: { + Name: string; + Contract: string; + RuleType: RuleType; + Enabled: boolean; + ID: string; + }; +} diff --git a/wails-frontend/src/pages/backupKeyPairs.tsx b/wails-frontend/src/pages/backupKeyPairs.tsx index 45758da54..48bc7d3d9 100644 --- a/wails-frontend/src/pages/backupKeyPairs.tsx +++ b/wails-frontend/src/pages/backupKeyPairs.tsx @@ -1,12 +1,13 @@ import { useState, useRef, SyntheticEvent } from 'react'; import { Password, Button, Clipboard } from '@massalabs/react-ui-kit'; +import { walletapp } from '@wailsjs/go/models'; import { SendPromptInput } from '@wailsjs/go/walletapp/WalletApp'; import { ClipboardSetText, EventsOnce } from '@wailsjs/runtime'; import { FiCopy, FiArrowRight } from 'react-icons/fi'; import { useLocation, useNavigate } from 'react-router-dom'; -import { events, promptRequest, promptResult } from '@/events/events'; +import { promptRequest, promptResult } from '@/events'; import Intl from '@/i18n/i18n'; import { Layout } from '@/layouts/Layout/Layout'; import { ErrorCode, IErrorObject, handleCancel } from '@/utils'; @@ -59,6 +60,8 @@ function BackupKeyPairs() { const [error, setError] = useState(null); + const { EventType } = walletapp; + function validate(e: SyntheticEvent) { const formObject = parseForm(e); const { password } = formObject; @@ -96,7 +99,7 @@ function BackupKeyPairs() { const formObject = parseForm(e); const { password } = formObject; - EventsOnce(events.promptResult, handleResult); + EventsOnce(EventType.promptResult, handleResult); await SendPromptInput(password); } diff --git a/wails-frontend/src/pages/backupMethods.tsx b/wails-frontend/src/pages/backupMethods.tsx index f740bd4f6..367e60dc9 100644 --- a/wails-frontend/src/pages/backupMethods.tsx +++ b/wails-frontend/src/pages/backupMethods.tsx @@ -1,12 +1,12 @@ import { useState } from 'react'; -import { Button } from '@massalabs/react-ui-kit'; -import { maskNickname } from '@massalabs/react-ui-kit/src/lib/massa-react/utils'; +import { Button, maskNickname } from '@massalabs/react-ui-kit'; +import { walletapp } from '@wailsjs/go/models'; import { SendPromptInput } from '@wailsjs/go/walletapp/WalletApp'; import { EventsOnce } from '@wailsjs/runtime/runtime'; import { useLocation, useNavigate } from 'react-router-dom'; -import { backupMethods, events, promptRequest } from '@/events/events'; +import { backupMethods, promptRequest } from '@/events'; import Intl from '@/i18n/i18n'; import { Layout } from '@/layouts/Layout/Layout'; import { handleApplyResult } from '@/utils'; @@ -20,9 +20,11 @@ function BackupMethods() { const req: promptRequest = state.req; const walletName: string = req.Msg; + const { EventType } = walletapp; + async function handleDownloadYaml() { EventsOnce( - events.promptResult, + EventType.promptResult, handleApplyResult(navigate, req, setErrorMsg, true), ); diff --git a/wails-frontend/src/pages/confirmDelete.tsx b/wails-frontend/src/pages/confirmDelete.tsx index 04f7ec252..4b12a9fbe 100644 --- a/wails-frontend/src/pages/confirmDelete.tsx +++ b/wails-frontend/src/pages/confirmDelete.tsx @@ -1,10 +1,11 @@ import { Button } from '@massalabs/react-ui-kit'; +import { walletapp } from '@wailsjs/go/models'; import { SendPromptInput } from '@wailsjs/go/walletapp/WalletApp'; import { EventsOnce } from '@wailsjs/runtime/runtime'; import { FiAlertTriangle, FiTrash2 } from 'react-icons/fi'; import { useLocation, useNavigate } from 'react-router-dom'; -import { events, promptRequest } from '@/events/events'; +import { promptRequest } from '@/events'; import { Layout } from '@/layouts/Layout/Layout'; import { handleApplyResult, handleCancel } from '@/utils'; @@ -14,8 +15,10 @@ function ConfirmDelete() { const req: promptRequest = state.req; const password = state.password; + const { EventType } = walletapp; + function handleConfirm() { - EventsOnce(events.promptResult, handleApplyResult(navigate, req)); + EventsOnce(EventType.promptResult, handleApplyResult(navigate, req)); SendPromptInput(password); } diff --git a/wails-frontend/src/pages/failure.tsx b/wails-frontend/src/pages/failure.tsx index 46cd97fcc..221946aa6 100644 --- a/wails-frontend/src/pages/failure.tsx +++ b/wails-frontend/src/pages/failure.tsx @@ -1,7 +1,7 @@ import { FiX } from 'react-icons/fi'; import { useLocation } from 'react-router'; -import { promptRequest } from '@/events/events'; +import { promptRequest } from '@/events'; import { Layout } from '@/layouts/Layout/Layout'; const Failure = () => { diff --git a/wails-frontend/src/pages/importFile.tsx b/wails-frontend/src/pages/importFile.tsx index 07bef45a6..6be23a29f 100644 --- a/wails-frontend/src/pages/importFile.tsx +++ b/wails-frontend/src/pages/importFile.tsx @@ -1,7 +1,6 @@ import { useState } from 'react'; -import { Button } from '@massalabs/react-ui-kit'; -import { maskNickname } from '@massalabs/react-ui-kit/src/lib/massa-react/utils'; +import { Button, maskNickname } from '@massalabs/react-ui-kit'; import { walletapp } from '@wailsjs/go/models'; import { SendPromptInput, @@ -10,7 +9,7 @@ import { import { EventsOnce } from '@wailsjs/runtime/runtime'; import { useLocation, useNavigate } from 'react-router-dom'; -import { events, promptRequest } from '@/events/events'; +import { promptRequest } from '@/events'; import Intl from '@/i18n/i18n'; import { Layout } from '@/layouts/Layout/Layout'; import { handleApplyResult, handleCancel } from '@/utils'; @@ -26,6 +25,8 @@ const ImportFile = () => { const { state } = useLocation(); const req: promptRequest = state.req; + const { EventType } = walletapp; + const accountStyleSuccess = 'mas-body text-s-success pb-4'; const accountStyleNormal = 'mas-body text-neutral pb-4'; @@ -50,7 +51,7 @@ const ImportFile = () => { setAccount(res); } else { EventsOnce( - events.promptResult, + EventType.promptResult, handleApplyResult(nav, req, setErrorMsg, true), ); await SendPromptInput(account.filePath); diff --git a/wails-frontend/src/pages/importMethods.tsx b/wails-frontend/src/pages/importMethods.tsx index eaa8bc6c4..b08e7a5a8 100644 --- a/wails-frontend/src/pages/importMethods.tsx +++ b/wails-frontend/src/pages/importMethods.tsx @@ -1,7 +1,7 @@ import { Button } from '@massalabs/react-ui-kit'; import { useLocation, useNavigate } from 'react-router-dom'; -import { promptRequest } from '@/events/events'; +import { promptRequest } from '@/events'; import { Layout } from '@/layouts/Layout/Layout'; const ImportMethods = () => { diff --git a/wails-frontend/src/pages/newPassword.tsx b/wails-frontend/src/pages/newPassword.tsx index 562eac872..13ed2dc31 100644 --- a/wails-frontend/src/pages/newPassword.tsx +++ b/wails-frontend/src/pages/newPassword.tsx @@ -1,6 +1,7 @@ import { useState, useRef, SyntheticEvent } from 'react'; import { Password, Button, Stepper } from '@massalabs/react-ui-kit'; +import { walletapp } from '@wailsjs/go/models'; import { SendPKeyPromptInput, SendPromptInput, @@ -9,7 +10,7 @@ import { EventsOnce } from '@wailsjs/runtime'; import { FiLock } from 'react-icons/fi'; import { useLocation, useNavigate } from 'react-router-dom'; -import { events, promptAction, promptRequest } from '@/events/events'; +import { promptRequest } from '@/events'; import { Layout } from '@/layouts/Layout/Layout'; import { parseForm, @@ -26,12 +27,14 @@ function NewPassword() { const { state } = useLocation(); const req: promptRequest = state.req; - const { newPasswordReq, importReq } = promptAction; + const { EventType } = walletapp; + + const { newPassword, import: importReq } = walletapp.PromptRequestAction; const isImportAction = req.Action === importReq; function getButtonLabel() { switch (req.Action) { - case newPasswordReq: + case newPassword: return 'Define'; case importReq: return 'Define and import'; @@ -42,7 +45,7 @@ function NewPassword() { function getSubtitle() { switch (req.Action) { - case newPasswordReq: + case newPassword: return 'Enter a secure password'; case importReq: return 'Define a new password'; @@ -83,7 +86,7 @@ function NewPassword() { const { password } = form; EventsOnce( - events.promptResult, + EventType.promptResult, handleApplyResult(navigate, req, setError, isImportAction), ); diff --git a/wails-frontend/src/pages/success.tsx b/wails-frontend/src/pages/success.tsx index d115404f9..a928947d7 100644 --- a/wails-frontend/src/pages/success.tsx +++ b/wails-frontend/src/pages/success.tsx @@ -1,7 +1,8 @@ +import { walletapp } from '@wailsjs/go/models'; import { FiCheck } from 'react-icons/fi'; import { useLocation } from 'react-router'; -import { promptAction, promptRequest } from '@/events/events'; +import { promptRequest } from '@/events'; import Intl from '@/i18n/i18n'; import { Layout } from '@/layouts/Layout/Layout'; @@ -9,17 +10,19 @@ function Success() { const { state } = useLocation(); const { req } = state; + const { PromptRequestAction } = walletapp; + function successMsg(req: promptRequest) { switch (req.Action) { - case promptAction.deleteReq: + case PromptRequestAction.delete: return Intl.t('success.delete'); - case promptAction.newPasswordReq: + case PromptRequestAction.newPassword: return Intl.t('success.new-password'); - case promptAction.importReq: + case PromptRequestAction.import: return Intl.t('success.import'); - case promptAction.signReq: + case PromptRequestAction.sign: return Intl.t('success.sign'); - case promptAction.backupReq: + case PromptRequestAction.backup: return Intl.t('success.backup'); default: return Intl.t('success.success'); diff --git a/wails-frontend/src/utils/handleApplyResult.tsx b/wails-frontend/src/utils/handleApplyResult.tsx index c373edfa7..424e35882 100644 --- a/wails-frontend/src/utils/handleApplyResult.tsx +++ b/wails-frontend/src/utils/handleApplyResult.tsx @@ -3,7 +3,7 @@ import { WindowReloadApp } from '@wailsjs/runtime/runtime'; import { NavigateFunction } from 'react-router-dom'; import { IErrorObject } from './errors'; -import { promptResult, promptRequest } from '@/events/events'; +import { promptResult, promptRequest } from '@/events'; import Intl from '@/i18n/i18n'; import { useConfigStore } from '@/store/store'; diff --git a/wails-frontend/wailsjs/go/models.ts b/wails-frontend/wailsjs/go/models.ts index af2ec967d..c83e807f0 100755 --- a/wails-frontend/wailsjs/go/models.ts +++ b/wails-frontend/wailsjs/go/models.ts @@ -1,5 +1,22 @@ export namespace walletapp { + export enum PromptRequestAction { + delete = "DELETE_ACCOUNT", + newPassword = "CREATE_PASSWORD", + sign = "SIGN", + import = "IMPORT_ACCOUNT", + backup = "BACKUP_ACCOUNT", + tradeRolls = "TRADE_ROLLS", + unprotect = "UNPROTECT", + addSignRule = "ADD_SIGN_RULE", + deleteSignRule = "DELETE_SIGN_RULE", + updateSignRule = "UPDATE_SIGN_RULE", + } + export enum EventType { + promptResult = "PROMPT_RESULT", + promptData = "PROMPT_DATA", + promptRequest = "PROMPT_REQUEST", + } export class selectFileResult { err: string; codeMessage: string;