diff --git a/api/api.go b/api/api.go index d4a6f974..2abd51b4 100644 --- a/api/api.go +++ b/api/api.go @@ -349,7 +349,9 @@ func (s *Server) setupRoutes(e *echo.Echo) { e.POST("/api/wallet/create", s.toEchoHandler(s.walletHandler.CreateHandler)) e.POST("/api/wallet", s.toEchoHandler(s.walletHandler.ImportHandler)) e.GET("/api/wallet", s.toEchoHandler(s.walletHandler.ListHandler)) + e.POST("/api/wallet/:address/init", s.toEchoHandler(s.walletHandler.InitHandler)) e.DELETE("/api/wallet/:address", s.toEchoHandler(s.walletHandler.RemoveHandler)) + e.PATCH("/api/wallet/:address", s.toEchoHandler(s.walletHandler.UpdateHandler)) // Wallet Association e.POST("/api/preparation/:id/wallet/:wallet", s.toEchoHandler(s.walletHandler.AttachHandler)) diff --git a/api/api_test.go b/api/api_test.go index da4f2ab1..f8768dd2 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -190,6 +190,8 @@ func setupMockWallet() wallet.Handler { Return(&model.Preparation{}, nil) m.On("ImportHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(&model.Wallet{}, nil) + m.On("InitHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&model.Wallet{}, nil) m.On("ListHandler", mock.Anything, mock.Anything). Return([]model.Wallet{{}}, nil) m.On("ListAttachedHandler", mock.Anything, mock.Anything, "id"). @@ -321,6 +323,15 @@ func TestAllAPIs(t *testing.T) { require.True(t, resp.IsSuccess()) require.NotNil(t, resp.Payload) }) + t.Run("InitWallet", func(t *testing.T) { + resp, err := client.Wallet.InitWallet(&wallet2.InitWalletParams{ + Context: ctx, + Address: "wallet", + }) + require.NoError(t, err) + require.True(t, resp.IsSuccess()) + require.NotNil(t, resp.Payload) + }) t.Run("ListWallets", func(t *testing.T) { resp, err := client.Wallet.ListWallets(&wallet2.ListWalletsParams{ Context: ctx, diff --git a/client/swagger/http/wallet/init_wallet_parameters.go b/client/swagger/http/wallet/init_wallet_parameters.go new file mode 100644 index 00000000..3a8fb76d --- /dev/null +++ b/client/swagger/http/wallet/init_wallet_parameters.go @@ -0,0 +1,151 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package wallet + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" +) + +// NewInitWalletParams creates a new InitWalletParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewInitWalletParams() *InitWalletParams { + return &InitWalletParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewInitWalletParamsWithTimeout creates a new InitWalletParams object +// with the ability to set a timeout on a request. +func NewInitWalletParamsWithTimeout(timeout time.Duration) *InitWalletParams { + return &InitWalletParams{ + timeout: timeout, + } +} + +// NewInitWalletParamsWithContext creates a new InitWalletParams object +// with the ability to set a context for a request. +func NewInitWalletParamsWithContext(ctx context.Context) *InitWalletParams { + return &InitWalletParams{ + Context: ctx, + } +} + +// NewInitWalletParamsWithHTTPClient creates a new InitWalletParams object +// with the ability to set a custom HTTPClient for a request. +func NewInitWalletParamsWithHTTPClient(client *http.Client) *InitWalletParams { + return &InitWalletParams{ + HTTPClient: client, + } +} + +/* +InitWalletParams contains all the parameters to send to the API endpoint + + for the init wallet operation. + + Typically these are written to a http.Request. +*/ +type InitWalletParams struct { + + /* Address. + + Address + */ + Address string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the init wallet params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *InitWalletParams) WithDefaults() *InitWalletParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the init wallet params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *InitWalletParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the init wallet params +func (o *InitWalletParams) WithTimeout(timeout time.Duration) *InitWalletParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the init wallet params +func (o *InitWalletParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the init wallet params +func (o *InitWalletParams) WithContext(ctx context.Context) *InitWalletParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the init wallet params +func (o *InitWalletParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the init wallet params +func (o *InitWalletParams) WithHTTPClient(client *http.Client) *InitWalletParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the init wallet params +func (o *InitWalletParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithAddress adds the address to the init wallet params +func (o *InitWalletParams) WithAddress(address string) *InitWalletParams { + o.SetAddress(address) + return o +} + +// SetAddress adds the address to the init wallet params +func (o *InitWalletParams) SetAddress(address string) { + o.Address = address +} + +// WriteToRequest writes these params to a swagger request +func (o *InitWalletParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param address + if err := r.SetPathParam("address", o.Address); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/swagger/http/wallet/init_wallet_responses.go b/client/swagger/http/wallet/init_wallet_responses.go new file mode 100644 index 00000000..6491c25e --- /dev/null +++ b/client/swagger/http/wallet/init_wallet_responses.go @@ -0,0 +1,258 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package wallet + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/data-preservation-programs/singularity/client/swagger/models" +) + +// InitWalletReader is a Reader for the InitWallet structure. +type InitWalletReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *InitWalletReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewInitWalletOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewInitWalletBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 500: + result := NewInitWalletInternalServerError() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("[POST /wallet/{address}/init] InitWallet", response, response.Code()) + } +} + +// NewInitWalletOK creates a InitWalletOK with default headers values +func NewInitWalletOK() *InitWalletOK { + return &InitWalletOK{} +} + +/* +InitWalletOK describes a response with status code 200, with default header values. + +OK +*/ +type InitWalletOK struct { + Payload *models.ModelWallet +} + +// IsSuccess returns true when this init wallet o k response has a 2xx status code +func (o *InitWalletOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this init wallet o k response has a 3xx status code +func (o *InitWalletOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this init wallet o k response has a 4xx status code +func (o *InitWalletOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this init wallet o k response has a 5xx status code +func (o *InitWalletOK) IsServerError() bool { + return false +} + +// IsCode returns true when this init wallet o k response a status code equal to that given +func (o *InitWalletOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the init wallet o k response +func (o *InitWalletOK) Code() int { + return 200 +} + +func (o *InitWalletOK) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /wallet/{address}/init][%d] initWalletOK %s", 200, payload) +} + +func (o *InitWalletOK) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /wallet/{address}/init][%d] initWalletOK %s", 200, payload) +} + +func (o *InitWalletOK) GetPayload() *models.ModelWallet { + return o.Payload +} + +func (o *InitWalletOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.ModelWallet) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewInitWalletBadRequest creates a InitWalletBadRequest with default headers values +func NewInitWalletBadRequest() *InitWalletBadRequest { + return &InitWalletBadRequest{} +} + +/* +InitWalletBadRequest describes a response with status code 400, with default header values. + +Bad Request +*/ +type InitWalletBadRequest struct { + Payload *models.APIHTTPError +} + +// IsSuccess returns true when this init wallet bad request response has a 2xx status code +func (o *InitWalletBadRequest) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this init wallet bad request response has a 3xx status code +func (o *InitWalletBadRequest) IsRedirect() bool { + return false +} + +// IsClientError returns true when this init wallet bad request response has a 4xx status code +func (o *InitWalletBadRequest) IsClientError() bool { + return true +} + +// IsServerError returns true when this init wallet bad request response has a 5xx status code +func (o *InitWalletBadRequest) IsServerError() bool { + return false +} + +// IsCode returns true when this init wallet bad request response a status code equal to that given +func (o *InitWalletBadRequest) IsCode(code int) bool { + return code == 400 +} + +// Code gets the status code for the init wallet bad request response +func (o *InitWalletBadRequest) Code() int { + return 400 +} + +func (o *InitWalletBadRequest) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /wallet/{address}/init][%d] initWalletBadRequest %s", 400, payload) +} + +func (o *InitWalletBadRequest) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /wallet/{address}/init][%d] initWalletBadRequest %s", 400, payload) +} + +func (o *InitWalletBadRequest) GetPayload() *models.APIHTTPError { + return o.Payload +} + +func (o *InitWalletBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.APIHTTPError) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewInitWalletInternalServerError creates a InitWalletInternalServerError with default headers values +func NewInitWalletInternalServerError() *InitWalletInternalServerError { + return &InitWalletInternalServerError{} +} + +/* +InitWalletInternalServerError describes a response with status code 500, with default header values. + +Internal Server Error +*/ +type InitWalletInternalServerError struct { + Payload *models.APIHTTPError +} + +// IsSuccess returns true when this init wallet internal server error response has a 2xx status code +func (o *InitWalletInternalServerError) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this init wallet internal server error response has a 3xx status code +func (o *InitWalletInternalServerError) IsRedirect() bool { + return false +} + +// IsClientError returns true when this init wallet internal server error response has a 4xx status code +func (o *InitWalletInternalServerError) IsClientError() bool { + return false +} + +// IsServerError returns true when this init wallet internal server error response has a 5xx status code +func (o *InitWalletInternalServerError) IsServerError() bool { + return true +} + +// IsCode returns true when this init wallet internal server error response a status code equal to that given +func (o *InitWalletInternalServerError) IsCode(code int) bool { + return code == 500 +} + +// Code gets the status code for the init wallet internal server error response +func (o *InitWalletInternalServerError) Code() int { + return 500 +} + +func (o *InitWalletInternalServerError) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /wallet/{address}/init][%d] initWalletInternalServerError %s", 500, payload) +} + +func (o *InitWalletInternalServerError) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /wallet/{address}/init][%d] initWalletInternalServerError %s", 500, payload) +} + +func (o *InitWalletInternalServerError) GetPayload() *models.APIHTTPError { + return o.Payload +} + +func (o *InitWalletInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.APIHTTPError) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/swagger/http/wallet/update_wallet_parameters.go b/client/swagger/http/wallet/update_wallet_parameters.go new file mode 100644 index 00000000..9875d065 --- /dev/null +++ b/client/swagger/http/wallet/update_wallet_parameters.go @@ -0,0 +1,175 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package wallet + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + + "github.com/data-preservation-programs/singularity/client/swagger/models" +) + +// NewUpdateWalletParams creates a new UpdateWalletParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewUpdateWalletParams() *UpdateWalletParams { + return &UpdateWalletParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewUpdateWalletParamsWithTimeout creates a new UpdateWalletParams object +// with the ability to set a timeout on a request. +func NewUpdateWalletParamsWithTimeout(timeout time.Duration) *UpdateWalletParams { + return &UpdateWalletParams{ + timeout: timeout, + } +} + +// NewUpdateWalletParamsWithContext creates a new UpdateWalletParams object +// with the ability to set a context for a request. +func NewUpdateWalletParamsWithContext(ctx context.Context) *UpdateWalletParams { + return &UpdateWalletParams{ + Context: ctx, + } +} + +// NewUpdateWalletParamsWithHTTPClient creates a new UpdateWalletParams object +// with the ability to set a custom HTTPClient for a request. +func NewUpdateWalletParamsWithHTTPClient(client *http.Client) *UpdateWalletParams { + return &UpdateWalletParams{ + HTTPClient: client, + } +} + +/* +UpdateWalletParams contains all the parameters to send to the API endpoint + + for the update wallet operation. + + Typically these are written to a http.Request. +*/ +type UpdateWalletParams struct { + + /* Address. + + Wallet address + */ + Address string + + /* Request. + + Request body + */ + Request *models.WalletUpdateRequest + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the update wallet params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UpdateWalletParams) WithDefaults() *UpdateWalletParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the update wallet params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UpdateWalletParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the update wallet params +func (o *UpdateWalletParams) WithTimeout(timeout time.Duration) *UpdateWalletParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the update wallet params +func (o *UpdateWalletParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the update wallet params +func (o *UpdateWalletParams) WithContext(ctx context.Context) *UpdateWalletParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the update wallet params +func (o *UpdateWalletParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the update wallet params +func (o *UpdateWalletParams) WithHTTPClient(client *http.Client) *UpdateWalletParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the update wallet params +func (o *UpdateWalletParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithAddress adds the address to the update wallet params +func (o *UpdateWalletParams) WithAddress(address string) *UpdateWalletParams { + o.SetAddress(address) + return o +} + +// SetAddress adds the address to the update wallet params +func (o *UpdateWalletParams) SetAddress(address string) { + o.Address = address +} + +// WithRequest adds the request to the update wallet params +func (o *UpdateWalletParams) WithRequest(request *models.WalletUpdateRequest) *UpdateWalletParams { + o.SetRequest(request) + return o +} + +// SetRequest adds the request to the update wallet params +func (o *UpdateWalletParams) SetRequest(request *models.WalletUpdateRequest) { + o.Request = request +} + +// WriteToRequest writes these params to a swagger request +func (o *UpdateWalletParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param address + if err := r.SetPathParam("address", o.Address); err != nil { + return err + } + if o.Request != nil { + if err := r.SetBodyParam(o.Request); err != nil { + return err + } + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/swagger/http/wallet/update_wallet_responses.go b/client/swagger/http/wallet/update_wallet_responses.go new file mode 100644 index 00000000..57cf35fd --- /dev/null +++ b/client/swagger/http/wallet/update_wallet_responses.go @@ -0,0 +1,334 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package wallet + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/data-preservation-programs/singularity/client/swagger/models" +) + +// UpdateWalletReader is a Reader for the UpdateWallet structure. +type UpdateWalletReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *UpdateWalletReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewUpdateWalletOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewUpdateWalletBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 404: + result := NewUpdateWalletNotFound() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 500: + result := NewUpdateWalletInternalServerError() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("[PATCH /wallet/{address}/update] UpdateWallet", response, response.Code()) + } +} + +// NewUpdateWalletOK creates a UpdateWalletOK with default headers values +func NewUpdateWalletOK() *UpdateWalletOK { + return &UpdateWalletOK{} +} + +/* +UpdateWalletOK describes a response with status code 200, with default header values. + +OK +*/ +type UpdateWalletOK struct { + Payload *models.ModelWallet +} + +// IsSuccess returns true when this update wallet o k response has a 2xx status code +func (o *UpdateWalletOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this update wallet o k response has a 3xx status code +func (o *UpdateWalletOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this update wallet o k response has a 4xx status code +func (o *UpdateWalletOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this update wallet o k response has a 5xx status code +func (o *UpdateWalletOK) IsServerError() bool { + return false +} + +// IsCode returns true when this update wallet o k response a status code equal to that given +func (o *UpdateWalletOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the update wallet o k response +func (o *UpdateWalletOK) Code() int { + return 200 +} + +func (o *UpdateWalletOK) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PATCH /wallet/{address}/update][%d] updateWalletOK %s", 200, payload) +} + +func (o *UpdateWalletOK) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PATCH /wallet/{address}/update][%d] updateWalletOK %s", 200, payload) +} + +func (o *UpdateWalletOK) GetPayload() *models.ModelWallet { + return o.Payload +} + +func (o *UpdateWalletOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.ModelWallet) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewUpdateWalletBadRequest creates a UpdateWalletBadRequest with default headers values +func NewUpdateWalletBadRequest() *UpdateWalletBadRequest { + return &UpdateWalletBadRequest{} +} + +/* +UpdateWalletBadRequest describes a response with status code 400, with default header values. + +Bad Request +*/ +type UpdateWalletBadRequest struct { + Payload *models.APIHTTPError +} + +// IsSuccess returns true when this update wallet bad request response has a 2xx status code +func (o *UpdateWalletBadRequest) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this update wallet bad request response has a 3xx status code +func (o *UpdateWalletBadRequest) IsRedirect() bool { + return false +} + +// IsClientError returns true when this update wallet bad request response has a 4xx status code +func (o *UpdateWalletBadRequest) IsClientError() bool { + return true +} + +// IsServerError returns true when this update wallet bad request response has a 5xx status code +func (o *UpdateWalletBadRequest) IsServerError() bool { + return false +} + +// IsCode returns true when this update wallet bad request response a status code equal to that given +func (o *UpdateWalletBadRequest) IsCode(code int) bool { + return code == 400 +} + +// Code gets the status code for the update wallet bad request response +func (o *UpdateWalletBadRequest) Code() int { + return 400 +} + +func (o *UpdateWalletBadRequest) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PATCH /wallet/{address}/update][%d] updateWalletBadRequest %s", 400, payload) +} + +func (o *UpdateWalletBadRequest) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PATCH /wallet/{address}/update][%d] updateWalletBadRequest %s", 400, payload) +} + +func (o *UpdateWalletBadRequest) GetPayload() *models.APIHTTPError { + return o.Payload +} + +func (o *UpdateWalletBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.APIHTTPError) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewUpdateWalletNotFound creates a UpdateWalletNotFound with default headers values +func NewUpdateWalletNotFound() *UpdateWalletNotFound { + return &UpdateWalletNotFound{} +} + +/* +UpdateWalletNotFound describes a response with status code 404, with default header values. + +Not Found +*/ +type UpdateWalletNotFound struct { + Payload *models.APIHTTPError +} + +// IsSuccess returns true when this update wallet not found response has a 2xx status code +func (o *UpdateWalletNotFound) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this update wallet not found response has a 3xx status code +func (o *UpdateWalletNotFound) IsRedirect() bool { + return false +} + +// IsClientError returns true when this update wallet not found response has a 4xx status code +func (o *UpdateWalletNotFound) IsClientError() bool { + return true +} + +// IsServerError returns true when this update wallet not found response has a 5xx status code +func (o *UpdateWalletNotFound) IsServerError() bool { + return false +} + +// IsCode returns true when this update wallet not found response a status code equal to that given +func (o *UpdateWalletNotFound) IsCode(code int) bool { + return code == 404 +} + +// Code gets the status code for the update wallet not found response +func (o *UpdateWalletNotFound) Code() int { + return 404 +} + +func (o *UpdateWalletNotFound) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PATCH /wallet/{address}/update][%d] updateWalletNotFound %s", 404, payload) +} + +func (o *UpdateWalletNotFound) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PATCH /wallet/{address}/update][%d] updateWalletNotFound %s", 404, payload) +} + +func (o *UpdateWalletNotFound) GetPayload() *models.APIHTTPError { + return o.Payload +} + +func (o *UpdateWalletNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.APIHTTPError) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewUpdateWalletInternalServerError creates a UpdateWalletInternalServerError with default headers values +func NewUpdateWalletInternalServerError() *UpdateWalletInternalServerError { + return &UpdateWalletInternalServerError{} +} + +/* +UpdateWalletInternalServerError describes a response with status code 500, with default header values. + +Internal Server Error +*/ +type UpdateWalletInternalServerError struct { + Payload *models.APIHTTPError +} + +// IsSuccess returns true when this update wallet internal server error response has a 2xx status code +func (o *UpdateWalletInternalServerError) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this update wallet internal server error response has a 3xx status code +func (o *UpdateWalletInternalServerError) IsRedirect() bool { + return false +} + +// IsClientError returns true when this update wallet internal server error response has a 4xx status code +func (o *UpdateWalletInternalServerError) IsClientError() bool { + return false +} + +// IsServerError returns true when this update wallet internal server error response has a 5xx status code +func (o *UpdateWalletInternalServerError) IsServerError() bool { + return true +} + +// IsCode returns true when this update wallet internal server error response a status code equal to that given +func (o *UpdateWalletInternalServerError) IsCode(code int) bool { + return code == 500 +} + +// Code gets the status code for the update wallet internal server error response +func (o *UpdateWalletInternalServerError) Code() int { + return 500 +} + +func (o *UpdateWalletInternalServerError) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PATCH /wallet/{address}/update][%d] updateWalletInternalServerError %s", 500, payload) +} + +func (o *UpdateWalletInternalServerError) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PATCH /wallet/{address}/update][%d] updateWalletInternalServerError %s", 500, payload) +} + +func (o *UpdateWalletInternalServerError) GetPayload() *models.APIHTTPError { + return o.Payload +} + +func (o *UpdateWalletInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.APIHTTPError) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/swagger/http/wallet/wallet_client.go b/client/swagger/http/wallet/wallet_client.go index e3373fe6..3c5a425e 100644 --- a/client/swagger/http/wallet/wallet_client.go +++ b/client/swagger/http/wallet/wallet_client.go @@ -60,10 +60,14 @@ type ClientService interface { ImportWallet(params *ImportWalletParams, opts ...ClientOption) (*ImportWalletOK, error) + InitWallet(params *InitWalletParams, opts ...ClientOption) (*InitWalletOK, error) + ListWallets(params *ListWalletsParams, opts ...ClientOption) (*ListWalletsOK, error) RemoveWallet(params *RemoveWalletParams, opts ...ClientOption) (*RemoveWalletNoContent, error) + UpdateWallet(params *UpdateWalletParams, opts ...ClientOption) (*UpdateWalletOK, error) + SetTransport(transport runtime.ClientTransport) } @@ -143,6 +147,44 @@ func (a *Client) ImportWallet(params *ImportWalletParams, opts ...ClientOption) panic(msg) } +/* +InitWallet initializes a newly created wallet +*/ +func (a *Client) InitWallet(params *InitWalletParams, opts ...ClientOption) (*InitWalletOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewInitWalletParams() + } + op := &runtime.ClientOperation{ + ID: "InitWallet", + Method: "POST", + PathPattern: "/wallet/{address}/init", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &InitWalletReader{formats: a.formats}, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*InitWalletOK) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for InitWallet: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + /* ListWallets lists all imported wallets */ @@ -219,6 +261,44 @@ func (a *Client) RemoveWallet(params *RemoveWalletParams, opts ...ClientOption) panic(msg) } +/* +UpdateWallet updates wallet details +*/ +func (a *Client) UpdateWallet(params *UpdateWalletParams, opts ...ClientOption) (*UpdateWalletOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewUpdateWalletParams() + } + op := &runtime.ClientOperation{ + ID: "UpdateWallet", + Method: "PATCH", + PathPattern: "/wallet/{address}/update", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &UpdateWalletReader{formats: a.formats}, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*UpdateWalletOK) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for UpdateWallet: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + // SetTransport changes the transport on the client func (a *Client) SetTransport(transport runtime.ClientTransport) { a.transport = transport diff --git a/client/swagger/models/wallet_create_request.go b/client/swagger/models/wallet_create_request.go index a1411264..f3dfd47f 100644 --- a/client/swagger/models/wallet_create_request.go +++ b/client/swagger/models/wallet_create_request.go @@ -17,8 +17,23 @@ import ( // swagger:model wallet.CreateRequest type WalletCreateRequest struct { - // This is either "secp256k1" or "bls" + // actor Id + ActorID string `json:"actorId,omitempty"` + + // For SPWallet creation + Address string `json:"address,omitempty"` + + // contact + Contact string `json:"contact,omitempty"` + + // For UserWallet creation (generates new keypair) KeyType string `json:"keyType,omitempty"` + + // location + Location string `json:"location,omitempty"` + + // Optional fields for adding details to Wallet + Name string `json:"name,omitempty"` } // Validate validates this wallet create request diff --git a/client/swagger/models/wallet_update_request.go b/client/swagger/models/wallet_update_request.go new file mode 100644 index 00000000..74c8d4ac --- /dev/null +++ b/client/swagger/models/wallet_update_request.go @@ -0,0 +1,56 @@ +// 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" +) + +// WalletUpdateRequest wallet update request +// +// swagger:model wallet.UpdateRequest +type WalletUpdateRequest struct { + + // Name is readable label for the wallet + ActorName string `json:"actorName,omitempty"` + + // Contact is optional email for SP wallets + ContactInfo string `json:"contactInfo,omitempty"` + + // Location is optional region, country for SP wallets + Location string `json:"location,omitempty"` +} + +// Validate validates this wallet update request +func (m *WalletUpdateRequest) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this wallet update request based on context it is used +func (m *WalletUpdateRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *WalletUpdateRequest) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *WalletUpdateRequest) UnmarshalBinary(b []byte) error { + var res WalletUpdateRequest + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/cmd/admin/migrate.go b/cmd/admin/migrate.go index ede8bf97..59436a4f 100644 --- a/cmd/admin/migrate.go +++ b/cmd/admin/migrate.go @@ -13,6 +13,20 @@ import ( var MigrateCmd = &cli.Command{ Name: "migrate", Usage: "Migrate database up, down, or to a certain version", + Before: func(c *cli.Context) error { + db, closer, err := database.OpenFromCLI(c) + if err != nil { + return errors.WithStack(err) + } + defer closer.Close() + + // Check if migrations table exists (indicates versioned migration strategy is in place) + if !db.Migrator().HasTable("migrations") { + return errors.New("Database has not been initialized with versioned migration strategy. Please run 'singularity admin init' first to migrate your database to the new migration system.") + } + + return nil + }, Subcommands: []*cli.Command{ { Name: "up", diff --git a/cmd/app.go b/cmd/app.go index d59a6319..b389c4ba 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -168,8 +168,10 @@ Upgrading: Subcommands: []*cli.Command{ wallet.CreateCmd, wallet.ImportCmd, + wallet.InitCmd, wallet.ListCmd, wallet.RemoveCmd, + wallet.UpdateCmd, }, }, { diff --git a/cmd/wallet/create.go b/cmd/wallet/create.go index 6c08f37c..0ce85bf4 100644 --- a/cmd/wallet/create.go +++ b/cmd/wallet/create.go @@ -5,6 +5,7 @@ import ( "github.com/data-preservation-programs/singularity/cmd/cliutil" "github.com/data-preservation-programs/singularity/database" "github.com/data-preservation-programs/singularity/handler/wallet" + "github.com/data-preservation-programs/singularity/util" "github.com/urfave/cli/v2" ) @@ -12,26 +13,52 @@ var CreateCmd = &cli.Command{ Name: "create", Usage: "Create a new wallet", ArgsUsage: "[type]", - Description: `Create a new Filecoin wallet using offline keypair generation. + Description: `Create a new Filecoin wallet or storage provider contact entry. -The wallet will be stored locally in the Singularity database and can be used for making deals and other operations. The private key is generated securely and stored encrypted. +The command automatically detects the wallet type based on provided arguments: +- For UserWallet: Creates a wallet with offline keypair generation +- For SPWallet: Creates a contact entry for a storage provider -SUPPORTED KEY TYPES: +SUPPORTED KEY TYPES (for UserWallet): secp256k1 ECDSA using the secp256k1 curve (default, most common) bls BLS signature scheme (Boneh-Lynn-Shacham) EXAMPLES: - # Create a secp256k1 wallet (default) + # Create a secp256k1 UserWallet (default) singularity wallet create - # Create a secp256k1 wallet explicitly + # Create a secp256k1 UserWallet explicitly singularity wallet create secp256k1 - # Create a BLS wallet + # Create a BLS UserWallet singularity wallet create bls + # Create an SPWallet contact entry + singularity wallet create --address f3abc123... --actor-id f01234 --name "Example SP" + The newly created wallet address and other details will be displayed upon successful creation.`, - Before: cliutil.CheckNArgs, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "address", + Usage: "Storage provider wallet address (creates SPWallet contact)", + }, + &cli.StringFlag{ + Name: "actor-id", + Usage: "Storage provider actor ID (e.g., f01234)", + }, + &cli.StringFlag{ + Name: "name", + Usage: "Optional display name", + }, + &cli.StringFlag{ + Name: "contact", + Usage: "Optional contact information", + }, + &cli.StringFlag{ + Name: "location", + Usage: "Optional provider location", + }, + }, Action: func(c *cli.Context) error { db, closer, err := database.OpenFromCLI(c) if err != nil { @@ -39,18 +66,32 @@ The newly created wallet address and other details will be displayed upon succes } defer closer.Close() - // Default to secp256k1 if no type is provided - keyType := c.Args().Get(0) - if keyType == "" { - keyType = wallet.KTSecp256k1.String() + request := wallet.CreateRequest{ + Name: c.String("name"), + Contact: c.String("contact"), + Location: c.String("location"), + } + + // Check if this is an SPWallet creation (has address or actor-id) + address := c.String("address") + actorID := c.String("actor-id") + + if address != "" || actorID != "" { + // Create SPWallet contact entry + request.Address = address + request.ActorID = actorID + } else { + // Create UserWallet with keypair generation + keyType := c.Args().Get(0) + if keyType == "" { + keyType = wallet.KTSecp256k1.String() + } + request.KeyType = keyType } - w, err := wallet.Default.CreateHandler( - c.Context, - db, - wallet.CreateRequest{ - KeyType: keyType, - }) + lotusClient := util.NewLotusClient(c.String("lotus-api"), c.String("lotus-token")) + + w, err := wallet.Default.CreateHandler(c.Context, db, lotusClient, request) if err != nil { return errors.WithStack(err) } diff --git a/cmd/wallet/init.go b/cmd/wallet/init.go new file mode 100644 index 00000000..097f1d7f --- /dev/null +++ b/cmd/wallet/init.go @@ -0,0 +1,32 @@ +package wallet + +import ( + "github.com/cockroachdb/errors" + "github.com/data-preservation-programs/singularity/cmd/cliutil" + "github.com/data-preservation-programs/singularity/database" + "github.com/data-preservation-programs/singularity/handler/wallet" + "github.com/data-preservation-programs/singularity/util" + "github.com/urfave/cli/v2" +) + +var InitCmd = &cli.Command{ + Name: "init", + Usage: "Initialize a wallet", + ArgsUsage: "
", + Before: cliutil.CheckNArgs, + Action: func(c *cli.Context) error { + db, closer, err := database.OpenFromCLI(c) + if err != nil { + return errors.WithStack(err) + } + defer closer.Close() + + lotusClient := util.NewLotusClient(c.String("lotus-api"), c.String("lotus-token")) + w, err := wallet.Default.InitHandler(c.Context, db, lotusClient, c.Args().Get(0)) + if err != nil { + return errors.WithStack(err) + } + cliutil.Print(c, w) + return nil + }, +} diff --git a/cmd/wallet/update.go b/cmd/wallet/update.go new file mode 100644 index 00000000..7ed94a35 --- /dev/null +++ b/cmd/wallet/update.go @@ -0,0 +1,88 @@ +package wallet + +import ( + "github.com/cockroachdb/errors" + "github.com/data-preservation-programs/singularity/cmd/cliutil" + "github.com/data-preservation-programs/singularity/database" + "github.com/data-preservation-programs/singularity/handler/wallet" + "github.com/gotidy/ptr" + "github.com/urfave/cli/v2" +) + +var UpdateCmd = &cli.Command{ + Name: "update", + Usage: "Update wallet details", + ArgsUsage: "", + Description: `Update non-essential details of an existing wallet. + +This command allows you to update the following wallet properties: +- Name (optional wallet label) +- Contact information (email for SP) +- Location (region, country for SP) + +Essential properties like the wallet address, private key, and balance cannot be modified. + +EXAMPLES: + # Update the actor name + singularity wallet update f1abc123... --name "My Main Wallet" + + # Update multiple fields at once + singularity wallet update f1xyz789... --name "Storage Provider" --location "US-East"`, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Usage: "Set the readable label for the wallet", + }, + &cli.StringFlag{ + Name: "contact", + Usage: "Set the contact information (email) for the wallet", + }, + &cli.StringFlag{ + Name: "location", + Usage: "Set the location (region, country) for the wallet", + }, + }, + Before: cliutil.CheckNArgs, + Action: func(c *cli.Context) error { + address := c.Args().Get(0) + + db, closer, err := database.OpenFromCLI(c) + if err != nil { + return errors.WithStack(err) + } + defer closer.Close() + + // Build the update request + request := wallet.UpdateRequest{} + + if c.IsSet("name") { + request.Name = ptr.Of(c.String("name")) + } + + if c.IsSet("contact") { + request.Contact = ptr.Of(c.String("contact")) + } + + if c.IsSet("location") { + request.Location = ptr.Of(c.String("location")) + } + + // Check if at least one field is provided for update + if request.Name == nil && request.Contact == nil && request.Location == nil { + return errors.New("at least one field must be provided for update") + } + + w, err := wallet.Default.UpdateHandler( + c.Context, + db, + address, + request, + ) + if err != nil { + return errors.WithStack(err) + } + + cliutil.Print(c, w) + return nil + }, +} diff --git a/cmd/wallet_test.go b/cmd/wallet_test.go index f10061d0..7ff2f7a2 100644 --- a/cmd/wallet_test.go +++ b/cmd/wallet_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "testing" + "github.com/cockroachdb/errors" "github.com/data-preservation-programs/singularity/cmd/cliutil" "github.com/data-preservation-programs/singularity/handler/wallet" "github.com/data-preservation-programs/singularity/model" @@ -30,17 +31,30 @@ func TestWalletCreate(t *testing.T) { mockHandler := new(wallet.MockWallet) defer swapWalletHandler(mockHandler)() mockHandler.On("CreateHandler", mock.Anything, mock.Anything, mock.Anything).Return(&model.Wallet{ - ActorID: "id", Address: "address", PrivateKey: "private", }, nil) _, _, err := runner.Run(ctx, "singularity wallet create") require.NoError(t, err) - _, _, err = runner.Run(ctx, "singularity --verbose wallet create") + _, _, err = runner.Run(ctx, "singularity wallet create secp256k1") + require.NoError(t, err) + _, _, err = runner.Run(ctx, "singularity wallet create bls") require.NoError(t, err) }) } +func TestWalletCreate_BadType(t *testing.T) { + testutil.OneWithoutReset(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + runner := NewRunner() + defer runner.Save(t) + mockHandler := new(wallet.MockWallet) + defer swapWalletHandler(mockHandler)() + mockHandler.On("CreateHandler", mock.Anything, mock.Anything, mock.Anything).Return((*model.Wallet)(nil), errors.New("unsupported key type: not-a-real-type")) + _, _, err := runner.Run(ctx, "singularity wallet create not-a-real-type") + require.Error(t, err) + }) +} + func TestWalletImport(t *testing.T) { testutil.OneWithoutReset(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { tmp := t.TempDir() @@ -62,6 +76,22 @@ func TestWalletImport(t *testing.T) { }) } +func TestWalletInit(t *testing.T) { + testutil.OneWithoutReset(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + runner := NewRunner() + defer runner.Save(t) + mockHandler := new(wallet.MockWallet) + defer swapWalletHandler(mockHandler)() + mockHandler.On("InitHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Wallet{ + ActorID: "id", + Address: "address", + PrivateKey: "private", + }, nil) + _, _, err := runner.Run(ctx, "singularity wallet init xxx") + require.NoError(t, err) + }) +} + func TestWalletList(t *testing.T) { testutil.OneWithoutReset(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { runner := NewRunner() @@ -107,3 +137,45 @@ func TestWalletRemove_NoReallyDoIt(t *testing.T) { require.ErrorIs(t, err, cliutil.ErrReallyDoIt) }) } + +func TestWalletUpdate(t *testing.T) { + testutil.OneWithoutReset(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + runner := NewRunner() + defer runner.Save(t) + mockHandler := new(wallet.MockWallet) + defer swapWalletHandler(mockHandler)() + mockHandler.On("UpdateHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Wallet{ + ActorID: "id", + ActorName: "Updated Name", + Address: "address", + ContactInfo: "test@example.com", + Location: "US-East", + WalletType: model.SPWallet, + }, nil) + _, _, err := runner.Run(ctx, "singularity wallet update --name Updated --contact test@example.com --location US-East address") + require.NoError(t, err) + + _, _, err = runner.Run(ctx, "singularity --verbose wallet update --name Updated address") + require.NoError(t, err) + }) +} + +func TestWalletUpdate_NoAddress(t *testing.T) { + testutil.OneWithoutReset(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + runner := NewRunner() + defer runner.Save(t) + _, _, err := runner.Run(ctx, "singularity wallet update --name Test") + require.Error(t, err) + require.Contains(t, err.Error(), "incorrect number of arguments") + }) +} + +func TestWalletUpdate_NoFields(t *testing.T) { + testutil.OneWithoutReset(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + runner := NewRunner() + defer runner.Save(t) + _, _, err := runner.Run(ctx, "singularity wallet update address") + require.Error(t, err) + require.Contains(t, err.Error(), "at least one field must be provided for update") + }) +} diff --git a/docs/en/SUMMARY.md b/docs/en/SUMMARY.md index c9d134c1..7529c188 100644 --- a/docs/en/SUMMARY.md +++ b/docs/en/SUMMARY.md @@ -69,8 +69,10 @@ * [Wallet](cli-reference/wallet/README.md) * [Create](cli-reference/wallet/create.md) * [Import](cli-reference/wallet/import.md) + * [Init](cli-reference/wallet/init.md) * [List](cli-reference/wallet/list.md) * [Remove](cli-reference/wallet/remove.md) + * [Update](cli-reference/wallet/update.md) * [Storage](cli-reference/storage/README.md) * [Create](cli-reference/storage/create/README.md) * [Acd](cli-reference/storage/create/acd.md) diff --git a/docs/en/cli-reference/wallet/README.md b/docs/en/cli-reference/wallet/README.md index 1710883b..588a39ec 100644 --- a/docs/en/cli-reference/wallet/README.md +++ b/docs/en/cli-reference/wallet/README.md @@ -11,8 +11,10 @@ USAGE: COMMANDS: create Create a new wallet import Import a wallet from exported private key + init Initialize a wallet list List all imported wallets remove Remove a wallet + update Update wallet details help, h Shows a list of commands or help for one command OPTIONS: diff --git a/docs/en/cli-reference/wallet/create.md b/docs/en/cli-reference/wallet/create.md index 743ffadd..dad33296 100644 --- a/docs/en/cli-reference/wallet/create.md +++ b/docs/en/cli-reference/wallet/create.md @@ -9,27 +9,37 @@ USAGE: singularity wallet create [command options] [type] DESCRIPTION: - Create a new Filecoin wallet using offline keypair generation. + Create a new Filecoin wallet or storage provider contact entry. - The wallet will be stored locally in the Singularity database and can be used for making deals and other operations. The private key is generated securely and stored encrypted. + The command automatically detects the wallet type based on provided arguments: + - For UserWallet: Creates a wallet with offline keypair generation + - For SPWallet: Creates a contact entry for a storage provider - SUPPORTED KEY TYPES: + SUPPORTED KEY TYPES (for UserWallet): secp256k1 ECDSA using the secp256k1 curve (default, most common) bls BLS signature scheme (Boneh-Lynn-Shacham) EXAMPLES: - # Create a secp256k1 wallet (default) + # Create a secp256k1 UserWallet (default) singularity wallet create - # Create a secp256k1 wallet explicitly + # Create a secp256k1 UserWallet explicitly singularity wallet create secp256k1 - # Create a BLS wallet + # Create a BLS UserWallet singularity wallet create bls + # Create an SPWallet contact entry + singularity wallet create --address f3abc123... --actor-id f01234 --name "Example SP" + The newly created wallet address and other details will be displayed upon successful creation. OPTIONS: - --help, -h show help + --address value Storage provider wallet address (creates SPWallet contact) + --actor-id value Storage provider actor ID (e.g., f01234) + --name value Optional display name + --contact value Optional contact information + --location value Optional provider location + --help, -h show help ``` {% endcode %} diff --git a/docs/en/cli-reference/wallet/init.md b/docs/en/cli-reference/wallet/init.md new file mode 100644 index 00000000..2b49de28 --- /dev/null +++ b/docs/en/cli-reference/wallet/init.md @@ -0,0 +1,14 @@ +# Initialize a wallet + +{% code fullWidth="true" %} +``` +NAME: + singularity wallet init - Initialize a wallet + +USAGE: + singularity wallet init [command options] + +OPTIONS: + --help, -h show help +``` +{% endcode %} diff --git a/docs/en/cli-reference/wallet/update.md b/docs/en/cli-reference/wallet/update.md new file mode 100644 index 00000000..4076195a --- /dev/null +++ b/docs/en/cli-reference/wallet/update.md @@ -0,0 +1,34 @@ +# Update wallet details + +{% code fullWidth="true" %} +``` +NAME: + singularity wallet update - Update wallet details + +USAGE: + singularity wallet update [command options] + +DESCRIPTION: + Update non-essential details of an existing wallet. + + This command allows you to update the following wallet properties: + - Name (optional wallet label) + - Contact information (email for SP) + - Location (region, country for SP) + + Essential properties like the wallet address, private key, and balance cannot be modified. + + EXAMPLES: + # Update the actor name + singularity wallet update f1abc123... --name "My Main Wallet" + + # Update multiple fields at once + singularity wallet update f1xyz789... --name "Storage Provider" --location "US-East" + +OPTIONS: + --name value Set the readable label for the wallet + --contact value Set the contact information (email) for the wallet + --location value Set the location (region, country) for the wallet + --help, -h show help +``` +{% endcode %} diff --git a/docs/en/web-api-reference/wallet.md b/docs/en/web-api-reference/wallet.md index 28a14954..89cb92c2 100644 --- a/docs/en/web-api-reference/wallet.md +++ b/docs/en/web-api-reference/wallet.md @@ -16,3 +16,11 @@ [https://raw.githubusercontent.com/data-preservation-programs/singularity/main/docs/swagger/swagger.yaml](https://raw.githubusercontent.com/data-preservation-programs/singularity/main/docs/swagger/swagger.yaml) {% endswagger %} +{% swagger src="https://raw.githubusercontent.com/data-preservation-programs/singularity/main/docs/swagger/swagger.yaml" path="/wallet/{address}/init" method="post" %} +[https://raw.githubusercontent.com/data-preservation-programs/singularity/main/docs/swagger/swagger.yaml](https://raw.githubusercontent.com/data-preservation-programs/singularity/main/docs/swagger/swagger.yaml) +{% endswagger %} + +{% swagger src="https://raw.githubusercontent.com/data-preservation-programs/singularity/main/docs/swagger/swagger.yaml" path="/wallet/{address}/update" method="patch" %} +[https://raw.githubusercontent.com/data-preservation-programs/singularity/main/docs/swagger/swagger.yaml](https://raw.githubusercontent.com/data-preservation-programs/singularity/main/docs/swagger/swagger.yaml) +{% endswagger %} + diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index f4db4ee4..949eba63 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -5612,6 +5612,106 @@ const docTemplate = `{ } } } + }, + "/wallet/{address}/init": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Wallet" + ], + "summary": "Initialize a newly created wallet", + "operationId": "InitWallet", + "parameters": [ + { + "type": "string", + "description": "Address", + "name": "address", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.Wallet" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.HTTPError" + } + } + } + } + }, + "/wallet/{address}/update": { + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Wallet" + ], + "summary": "Update wallet details", + "operationId": "UpdateWallet", + "parameters": [ + { + "type": "string", + "description": "Wallet address", + "name": "address", + "in": "path", + "required": true + }, + { + "description": "Request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/wallet.UpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.Wallet" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.HTTPError" + } + } + } + } } }, "definitions": { @@ -16496,8 +16596,25 @@ const docTemplate = `{ "wallet.CreateRequest": { "type": "object", "properties": { + "actorId": { + "type": "string" + }, + "address": { + "description": "For SPWallet creation", + "type": "string" + }, + "contact": { + "type": "string" + }, "keyType": { - "description": "This is either \"secp256k1\" or \"bls\"", + "description": "For UserWallet creation (generates new keypair)", + "type": "string" + }, + "location": { + "type": "string" + }, + "name": { + "description": "Optional fields for adding details to Wallet", "type": "string" } } @@ -16510,6 +16627,23 @@ const docTemplate = `{ "type": "string" } } + }, + "wallet.UpdateRequest": { + "type": "object", + "properties": { + "actorName": { + "description": "Name is readable label for the wallet", + "type": "string" + }, + "contactInfo": { + "description": "Contact is optional email for SP wallets", + "type": "string" + }, + "location": { + "description": "Location is optional region, country for SP wallets", + "type": "string" + } + } } }, "externalDocs": { diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 5244a1e3..ee06c39f 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -5606,6 +5606,106 @@ } } } + }, + "/wallet/{address}/init": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Wallet" + ], + "summary": "Initialize a newly created wallet", + "operationId": "InitWallet", + "parameters": [ + { + "type": "string", + "description": "Address", + "name": "address", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.Wallet" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.HTTPError" + } + } + } + } + }, + "/wallet/{address}/update": { + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Wallet" + ], + "summary": "Update wallet details", + "operationId": "UpdateWallet", + "parameters": [ + { + "type": "string", + "description": "Wallet address", + "name": "address", + "in": "path", + "required": true + }, + { + "description": "Request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/wallet.UpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.Wallet" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.HTTPError" + } + } + } + } } }, "definitions": { @@ -16490,8 +16590,25 @@ "wallet.CreateRequest": { "type": "object", "properties": { + "actorId": { + "type": "string" + }, + "address": { + "description": "For SPWallet creation", + "type": "string" + }, + "contact": { + "type": "string" + }, "keyType": { - "description": "This is either \"secp256k1\" or \"bls\"", + "description": "For UserWallet creation (generates new keypair)", + "type": "string" + }, + "location": { + "type": "string" + }, + "name": { + "description": "Optional fields for adding details to Wallet", "type": "string" } } @@ -16504,6 +16621,23 @@ "type": "string" } } + }, + "wallet.UpdateRequest": { + "type": "object", + "properties": { + "actorName": { + "description": "Name is readable label for the wallet", + "type": "string" + }, + "contactInfo": { + "description": "Contact is optional email for SP wallets", + "type": "string" + }, + "location": { + "description": "Location is optional region, country for SP wallets", + "type": "string" + } + } } }, "externalDocs": { diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index de07188a..56d99cec 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -8212,8 +8212,20 @@ definitions: type: object wallet.CreateRequest: properties: + actorId: + type: string + address: + description: For SPWallet creation + type: string + contact: + type: string keyType: - description: This is either "secp256k1" or "bls" + description: For UserWallet creation (generates new keypair) + type: string + location: + type: string + name: + description: Optional fields for adding details to Wallet type: string type: object wallet.ImportRequest: @@ -8222,6 +8234,18 @@ definitions: description: This is the exported private key from lotus wallet export type: string type: object + wallet.UpdateRequest: + properties: + actorName: + description: Name is readable label for the wallet + type: string + contactInfo: + description: Contact is optional email for SP wallets + type: string + location: + description: Location is optional region, country for SP wallets + type: string + type: object externalDocs: description: OpenAPI url: https://swagger.io/resources/open-api/ @@ -11886,6 +11910,72 @@ paths: summary: Remove a wallet tags: - Wallet + /wallet/{address}/init: + post: + operationId: InitWallet + parameters: + - description: Address + in: path + name: address + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/model.Wallet' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.HTTPError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.HTTPError' + summary: Initialize a newly created wallet + tags: + - Wallet + /wallet/{address}/update: + patch: + consumes: + - application/json + operationId: UpdateWallet + parameters: + - description: Wallet address + in: path + name: address + required: true + type: string + - description: Request body + in: body + name: request + required: true + schema: + $ref: '#/definitions/wallet.UpdateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/model.Wallet' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.HTTPError' + "404": + description: Not Found + schema: + $ref: '#/definitions/api.HTTPError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.HTTPError' + summary: Update wallet details + tags: + - Wallet /wallet/create: post: consumes: diff --git a/go.mod b/go.mod index fc1ffe8c..13d0a4d0 100644 --- a/go.mod +++ b/go.mod @@ -86,9 +86,13 @@ require ( ) require ( + github.com/bitfield/gotestdox v0.2.2 // indirect + github.com/dnephin/pflag v1.0.7 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/shirou/gopsutil/v3 v3.23.3 // indirect golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect + gotest.tools/gotestsum v1.12.2 // indirect ) require ( diff --git a/go.sum b/go.sum index 8d929c15..1608f837 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CDsnE= +github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/brianvoe/gofakeit/v6 v6.23.2 h1:lVde18uhad5wII/f5RMVFLtdQNE0HaGFuBUXmYKk8i8= github.com/brianvoe/gofakeit/v6 v6.23.2/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= @@ -162,6 +164,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3 github.com/dlespiau/covertool v0.0.0-20180314162135-b0c4c6d0583a/go.mod h1:/eQMcW3eA1bzKx23ZYI2H3tXPdJB5JWYTHzoUPBvQY4= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk= +github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY= @@ -466,6 +470,8 @@ github.com/google/pprof v0.0.0-20250202011525-fc3143867406/go.mod h1:vavhavw2zAx github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -1763,6 +1769,8 @@ gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/gotestsum v1.12.2 h1:eli4tu9Q2D/ogDsEGSr8XfQfl7mT0JsGOG6DFtUiZ/Q= +gotest.tools/gotestsum v1.12.2/go.mod h1:kjRtCglPZVsSU0hFHX3M5VWBM6Y63emHuB14ER1/sow= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= diff --git a/handler/wallet/create.go b/handler/wallet/create.go index 3bd7f118..f7c5a0eb 100644 --- a/handler/wallet/create.go +++ b/handler/wallet/create.go @@ -15,6 +15,7 @@ import ( "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-crypto" g1 "github.com/phoreproject/bls/g1pubs" + "github.com/ybbus/jsonrpc/v3" "golang.org/x/xerrors" "gorm.io/gorm" ) @@ -99,7 +100,17 @@ func GenerateKey(keyType string) (string, string, error) { } type CreateRequest struct { - KeyType string `json:"keyType"` // This is either "secp256k1" or "bls" + // For UserWallet creation (generates new keypair) + KeyType string `json:"keyType,omitempty"` // This is either "secp256k1" or "bls" + + // For SPWallet creation + Address string `json:"address,omitempty"` + ActorID string `json:"actorId,omitempty"` + + // Optional fields for adding details to Wallet + Name string `json:"name,omitempty"` + Contact string `json:"contact,omitempty"` + Location string `json:"location,omitempty"` } // @ID CreateWallet @@ -114,33 +125,93 @@ type CreateRequest struct { // @Router /wallet/create [post] func _() {} -// CreateHandler creates a new wallet using offline keypair generation and a new record in the local database. +// CreateHandler creates a new wallet and stores it in the local database. +// The wallet type is automatically inferred from the provided parameters: +// - If KeyType is provided: creates a UserWallet with generated keypair +// - If Address is provided: creates an SPWallet contact entry // // Parameters: // - ctx: The context for database transactions and other operations. // - db: A pointer to the gorm.DB instance representing the database connection. +// - lotusClient: The RPC client used to interact with a Lotus node for actor lookup (only used for SP wallets). +// - request: CreateRequest with either KeyType (for UserWallet) or Address (for SPWallet) // // Returns: // - A pointer to the created Wallet model if successful. -// - An error, if any occurred during the database insert operation. +// - An error, if any occurred during validation or database operations. func (DefaultHandler) CreateHandler( ctx context.Context, db *gorm.DB, + lotusClient jsonrpc.RPCClient, request CreateRequest, ) (*model.Wallet, error) { db = db.WithContext(ctx) - // Generate a new keypair - privateKey, address, err := GenerateKey(request.KeyType) - if err != nil { - return nil, errors.WithStack(err) + // Infer wallet type from provided parameters + hasKeyType := request.KeyType != "" + hasAddress := request.Address != "" + hasActorID := request.ActorID != "" + + // Validate that only one wallet type is specified + switch { + case !hasKeyType && !hasAddress && !hasActorID: + return nil, errors.New("must specify either KeyType (for UserWallet) or Address/ActorID (for SPWallet)") + case !hasKeyType && !(hasAddress && hasActorID): + return nil, errors.New("must specify both Address and ActorID (for SPWallet)") + case hasKeyType && (hasAddress || hasActorID): + return nil, errors.New("cannot specify both KeyType (for UserWallet) and Address/ActorID (for SPWallet) - please specify parameters for one wallet type") } - wallet := model.Wallet{ - Address: address, - PrivateKey: privateKey, + var wallet model.Wallet + + if hasKeyType { + // Create UserWallet: generate a new keypair + privateKey, address, err := GenerateKey(request.KeyType) + if err != nil { + return nil, errors.WithStack(err) + } + + wallet = model.Wallet{ + Address: address, + PrivateKey: privateKey, + WalletType: model.UserWallet, + // ActorID is empty for UserWallets until initialized + } + } else { + // Validate the address and actor ID with Lotus + addr, err := address.NewFromString(request.Address) + if err != nil { + return nil, errors.Wrap(handlererror.ErrInvalidParameter, "invalid address format") + } + + var result string + err = lotusClient.CallFor(ctx, &result, "Filecoin.StateLookupID", addr.String(), nil) + if err != nil { + logger.Errorw("failed to lookup state for wallet address", "addr", addr, "err", err) + return nil, errors.Join(handlererror.ErrInvalidParameter, errors.Wrap(err, "failed to lookup actor ID")) + } + + _, err = address.NewFromString(result) + if err != nil { + return nil, errors.Wrap(handlererror.ErrInvalidParameter, "invalid actor ID") + } else if result != request.ActorID { + return nil, errors.Wrap(handlererror.ErrInvalidParameter, "provided actor ID is not associated with address") + } + + wallet = model.Wallet{ + ActorID: result, + Address: result[:1] + addr.String()[1:], + WalletType: model.SPWallet, + // PrivateKey is empty for SP wallets + } } - err = database.DoRetry(ctx, func() error { + + // Update wallet details + wallet.ActorName = request.Name + wallet.ContactInfo = request.Contact + wallet.Location = request.Location + + err := database.DoRetry(ctx, func() error { return db.Create(&wallet).Error }) if util.IsDuplicateKeyError(err) { diff --git a/handler/wallet/create_test.go b/handler/wallet/create_test.go index a966c48f..995b9126 100644 --- a/handler/wallet/create_test.go +++ b/handler/wallet/create_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/data-preservation-programs/singularity/util" "github.com/data-preservation-programs/singularity/util/testutil" "github.com/stretchr/testify/require" "gorm.io/gorm" @@ -11,25 +12,107 @@ import ( func TestCreateHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - t.Run("success-secp256k1", func(t *testing.T) { - w, err := Default.CreateHandler(ctx, db, CreateRequest{KeyType: KTSecp256k1.String()}) + lotusClient := util.NewLotusClient("https://api.node.glif.io/rpc/v0", "") + + t.Run("success-user-wallet-secp256k1", func(t *testing.T) { + w, err := Default.CreateHandler(ctx, db, lotusClient, CreateRequest{ + KeyType: KTSecp256k1.String(), + }) require.NoError(t, err) require.NotEmpty(t, w.Address) require.Equal(t, "f1", w.Address[:2]) require.NotEmpty(t, w.PrivateKey) + require.Equal(t, "UserWallet", string(w.WalletType)) }) - t.Run("success-bls", func(t *testing.T) { - w, err := Default.CreateHandler(ctx, db, CreateRequest{KeyType: KTBLS.String()}) + t.Run("success-user-wallet-bls", func(t *testing.T) { + w, err := Default.CreateHandler(ctx, db, lotusClient, CreateRequest{ + KeyType: KTBLS.String(), + }) require.NoError(t, err) require.NotEmpty(t, w.Address) require.Equal(t, "f3", w.Address[:2]) require.NotEmpty(t, w.PrivateKey) + require.Equal(t, "UserWallet", string(w.WalletType)) + }) + + t.Run("success-user-wallet-with-details", func(t *testing.T) { + w, err := Default.CreateHandler(ctx, db, lotusClient, CreateRequest{ + KeyType: KTSecp256k1.String(), + Name: "my wallet", + }) + require.NoError(t, err) + require.NotEmpty(t, w.Address) + require.Equal(t, "f1", w.Address[:2]) + require.NotEmpty(t, w.PrivateKey) + require.Equal(t, "UserWallet", string(w.WalletType)) + require.Equal(t, "my wallet", w.ActorName) + }) + t.Run("success-sp-wallet", func(t *testing.T) { + w, err := Default.CreateHandler(ctx, db, lotusClient, CreateRequest{ + Address: testutil.TestWalletAddr, + ActorID: testutil.TestWalletActorID, + Name: "Test SP", + Contact: "test@example.com", + Location: "US", + }) + require.NoError(t, err) + require.Equal(t, testutil.TestWalletAddr, w.Address) + require.Equal(t, testutil.TestWalletActorID, w.ActorID) + require.Equal(t, "Test SP", w.ActorName) + require.Equal(t, "test@example.com", w.ContactInfo) + require.Equal(t, "US", w.Location) + require.Empty(t, w.PrivateKey) + require.Equal(t, "SPWallet", string(w.WalletType)) + }) + + t.Run("error-no-parameters", func(t *testing.T) { + _, err := Default.CreateHandler(ctx, db, lotusClient, CreateRequest{}) + require.Error(t, err) + require.Contains(t, err.Error(), "must specify either KeyType (for UserWallet) or Address/ActorID (for SPWallet)") + }) + + t.Run("error-sp-wallet-missing-actorid", func(t *testing.T) { + _, err := Default.CreateHandler(ctx, db, lotusClient, CreateRequest{ + Address: "f123456789", + Name: "Test SP", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "must specify both Address and ActorID (for SPWallet)") + }) + + t.Run("error-sp-wallet-missing-address", func(t *testing.T) { + _, err := Default.CreateHandler(ctx, db, lotusClient, CreateRequest{ + ActorID: "f1234", + Name: "Test SP", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "must specify both Address and ActorID (for SPWallet)") + }) + + t.Run("error-sp-wallet-mismatched-id", func(t *testing.T) { + _, err := Default.CreateHandler(ctx, db, lotusClient, CreateRequest{ + Address: testutil.TestWalletAddr, + ActorID: "wrong-actor-id", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "provided actor ID is not associated with address") }) t.Run("invalid-key-type", func(t *testing.T) { - _, err := Default.CreateHandler(ctx, db, CreateRequest{KeyType: "invalid-type"}) + _, err := Default.CreateHandler(ctx, db, lotusClient, CreateRequest{ + KeyType: "invalid-type", + }) + require.Error(t, err) + }) + + t.Run("error-mixed-parameters", func(t *testing.T) { + _, err := Default.CreateHandler(ctx, db, lotusClient, CreateRequest{ + KeyType: KTSecp256k1.String(), + Address: "f3abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + }) require.Error(t, err) + require.Contains(t, err.Error(), "cannot specify both KeyType (for UserWallet) and Address/ActorID (for SPWallet)") }) }) } diff --git a/handler/wallet/init.go b/handler/wallet/init.go new file mode 100644 index 00000000..d1f5d41d --- /dev/null +++ b/handler/wallet/init.go @@ -0,0 +1,68 @@ +package wallet + +import ( + "context" + + "github.com/cockroachdb/errors" + "github.com/data-preservation-programs/singularity/database" + "github.com/data-preservation-programs/singularity/handler/handlererror" + "github.com/data-preservation-programs/singularity/model" + "github.com/ybbus/jsonrpc/v3" + "gorm.io/gorm" +) + +// @ID InitWallet +// @Summary Initialize a newly created wallet +// @Tags Wallet +// @Produce json +// @Param address path string true "Address" +// @Success 200 {object} model.Wallet +// @Failure 400 {object} api.HTTPError +// @Failure 500 {object} api.HTTPError +// @Router /wallet/{address}/init [post] +func _() {} + +// InitHandler marks a new wallet created from offline keypair generation as initialized by updating its ActorID +// +// Parameters: +// - ctx: The context for database transactions and other operations. +// - db: A pointer to the gorm.DB instance representing the database connection. +// - lotusClient: The RPC client used to interact with a Lotus node for actor lookup. +// - address: The address or ID of the wallet to be initialized. +// +// Returns: +// - A pointer to the initialized Wallet model if successful. +// - An error, if any occurred during the database insert operation. +func (DefaultHandler) InitHandler( + ctx context.Context, + db *gorm.DB, + lotusClient jsonrpc.RPCClient, + address string, +) (*model.Wallet, error) { + db = db.WithContext(ctx) + var wallet model.Wallet + err := wallet.FindByIDOrAddr(db, address) + if err != nil { + return nil, errors.Wrap(err, "failed to find wallet") + } + + if wallet.ActorID != "" { + return &wallet, nil + } + + var result string + err = lotusClient.CallFor(ctx, &result, "Filecoin.StateLookupID", wallet.Address, nil) + if err != nil { + return nil, errors.Join(handlererror.ErrInvalidParameter, errors.Wrap(err, "failed to lookup actor ID")) + } + + wallet.ActorID = result + err = database.DoRetry(ctx, func() error { + return db.Save(&wallet).Error + }) + if err != nil { + return nil, errors.WithStack(err) + } + + return &wallet, nil +} diff --git a/handler/wallet/init_test.go b/handler/wallet/init_test.go new file mode 100644 index 00000000..6ac53eee --- /dev/null +++ b/handler/wallet/init_test.go @@ -0,0 +1,50 @@ +package wallet + +import ( + "context" + "testing" + + "github.com/data-preservation-programs/singularity/model" + "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/testutil" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +func TestInitHandler(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + lotusClient := util.NewLotusClient("https://api.node.glif.io/rpc/v0", "") + + t.Run("success", func(t *testing.T) { + err := db.Create(&model.Wallet{ + Address: testutil.TestWalletAddr, + PrivateKey: testutil.TestPrivateKeyHex, + }).Error + require.NoError(t, err) + w, err := Default.InitHandler(ctx, db, lotusClient, testutil.TestWalletAddr) + require.NoError(t, err) + require.NotEmpty(t, w.PrivateKey) + require.Equal(t, w.Address, testutil.TestWalletAddr) + require.NotEmpty(t, w.ActorID) + + // Running again on an initialized wallet should not change the wallet + w2, err := Default.InitHandler(ctx, db, lotusClient, testutil.TestWalletAddr) + require.NoError(t, err) + require.Equal(t, w.ActorID, w2.ActorID) + }) + + t.Run("uninitialized-address", func(t *testing.T) { + err := db.Create(&model.Wallet{ + Address: "f100", + }).Error + require.NoError(t, err) + _, err = Default.InitHandler(ctx, db, lotusClient, "f100") + require.ErrorContains(t, err, "failed to lookup actor ID") + }) + + t.Run("unknown-address", func(t *testing.T) { + _, err := Default.InitHandler(ctx, db, lotusClient, "unknown-address") + require.ErrorContains(t, err, "failed to find wallet") + }) + }) +} diff --git a/handler/wallet/interface.go b/handler/wallet/interface.go index 0a88d848..bc8f6329 100644 --- a/handler/wallet/interface.go +++ b/handler/wallet/interface.go @@ -20,6 +20,7 @@ type Handler interface { CreateHandler( ctx context.Context, db *gorm.DB, + lotusClient jsonrpc.RPCClient, request CreateRequest, ) (*model.Wallet, error) DetachHandler( @@ -34,6 +35,12 @@ type Handler interface { lotusClient jsonrpc.RPCClient, request ImportRequest, ) (*model.Wallet, error) + InitHandler( + ctx context.Context, + db *gorm.DB, + lotusClient jsonrpc.RPCClient, + address string, + ) (*model.Wallet, error) ListHandler( ctx context.Context, db *gorm.DB, @@ -48,6 +55,12 @@ type Handler interface { db *gorm.DB, address string, ) error + UpdateHandler( + ctx context.Context, + db *gorm.DB, + address string, + request UpdateRequest, + ) (*model.Wallet, error) } type DefaultHandler struct{} @@ -65,7 +78,7 @@ func (m *MockWallet) AttachHandler(ctx context.Context, db *gorm.DB, preparation return args.Get(0).(*model.Preparation), args.Error(1) } -func (m *MockWallet) CreateHandler(ctx context.Context, db *gorm.DB, request CreateRequest) (*model.Wallet, error) { +func (m *MockWallet) CreateHandler(ctx context.Context, db *gorm.DB, lotusClient jsonrpc.RPCClient, request CreateRequest) (*model.Wallet, error) { args := m.Called(ctx, db, request) return args.Get(0).(*model.Wallet), args.Error(1) } @@ -80,6 +93,11 @@ func (m *MockWallet) ImportHandler(ctx context.Context, db *gorm.DB, lotusClient return args.Get(0).(*model.Wallet), args.Error(1) } +func (m *MockWallet) InitHandler(ctx context.Context, db *gorm.DB, lotusClient jsonrpc.RPCClient, wallet string) (*model.Wallet, error) { + args := m.Called(ctx, db, lotusClient, wallet) + return args.Get(0).(*model.Wallet), args.Error(1) +} + func (m *MockWallet) ListHandler(ctx context.Context, db *gorm.DB) ([]model.Wallet, error) { args := m.Called(ctx, db) return args.Get(0).([]model.Wallet), args.Error(1) @@ -94,3 +112,8 @@ func (m *MockWallet) RemoveHandler(ctx context.Context, db *gorm.DB, address str args := m.Called(ctx, db, address) return args.Error(0) } + +func (m *MockWallet) UpdateHandler(ctx context.Context, db *gorm.DB, address string, request UpdateRequest) (*model.Wallet, error) { + args := m.Called(ctx, db, address, request) + return args.Get(0).(*model.Wallet), args.Error(1) +} diff --git a/handler/wallet/update.go b/handler/wallet/update.go new file mode 100644 index 00000000..776cbbb9 --- /dev/null +++ b/handler/wallet/update.go @@ -0,0 +1,101 @@ +package wallet + +import ( + "context" + + "github.com/cockroachdb/errors" + "github.com/data-preservation-programs/singularity/database" + "github.com/data-preservation-programs/singularity/handler/handlererror" + "github.com/data-preservation-programs/singularity/model" + "gorm.io/gorm" +) + +type UpdateRequest struct { + Name *string `json:"actorName,omitempty"` // Name is readable label for the wallet + Contact *string `json:"contactInfo,omitempty"` // Contact is optional email for SP wallets + Location *string `json:"location,omitempty"` // Location is optional region, country for SP wallets +} + +// @ID UpdateWallet +// @Summary Update wallet details +// @Tags Wallet +// @Accept json +// @Produce json +// @Param address path string true "Wallet address" +// @Param request body UpdateRequest true "Request body" +// @Success 200 {object} model.Wallet +// @Failure 400 {object} api.HTTPError +// @Failure 404 {object} api.HTTPError +// @Failure 500 {object} api.HTTPError +// @Router /wallet/{address}/update [patch] +func _() {} + +// UpdateHandler updates non-essential details of an existing wallet. +// Only fields provided in the request will be updated (partial update). +// Essential fields like Address, PrivateKey, and Balance cannot be modified. +// +// Parameters: +// - ctx: The context for database transactions and other operations. +// - db: A pointer to the gorm.DB instance representing the database connection. +// - address: The wallet address to update. +// - request: UpdateRequest containing the fields to update. +// +// Returns: +// - A pointer to the updated Wallet model if successful. +// - An error, if any occurred during the database operation. +func (DefaultHandler) UpdateHandler( + ctx context.Context, + db *gorm.DB, + address string, + request UpdateRequest, +) (*model.Wallet, error) { + db = db.WithContext(ctx) + + // Find the existing wallet + var wallet model.Wallet + err := wallet.FindByIDOrAddr(db, address) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrap(handlererror.ErrNotFound, "wallet not found") + } + if err != nil { + return nil, errors.WithStack(err) + } + + // Prepare update data - only include fields that are provided + updates := make(map[string]interface{}) + + if request.Name != nil { + updates["actor_name"] = *request.Name + } + + if request.Contact != nil { + updates["contact_info"] = *request.Contact + } + + if request.Location != nil { + updates["location"] = *request.Location + } + + // If no fields to update, return the existing wallet + if len(updates) == 0 { + return &wallet, nil + } + + // Perform the update + err = database.DoRetry(ctx, func() error { + return db.Model(&wallet).Updates(updates).Error + }) + if err != nil { + return nil, errors.WithStack(err) + } + + // Fetch the updated wallet to return + err = database.DoRetry(ctx, func() error { + return db.Where("address = ?", address).First(&wallet).Error + }) + if err != nil { + return nil, errors.WithStack(err) + } + + return &wallet, nil +} diff --git a/handler/wallet/update_test.go b/handler/wallet/update_test.go new file mode 100644 index 00000000..a1c0e08e --- /dev/null +++ b/handler/wallet/update_test.go @@ -0,0 +1,108 @@ +package wallet + +import ( + "context" + "testing" + + "github.com/data-preservation-programs/singularity/model" + "github.com/data-preservation-programs/singularity/util/testutil" + "github.com/gotidy/ptr" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +func TestDefaultHandler_UpdateHandler(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + // Create a test wallet first + wallet := model.Wallet{ + ActorID: "test-id", + ActorName: "Original Name", + Address: "f1test123address", + ContactInfo: "original@example.com", + Location: "Original Location", + WalletType: model.SPWallet, + } + err := db.Create(&wallet).Error + require.NoError(t, err) + + handler := DefaultHandler{} + + t.Run("update all fields", func(t *testing.T) { + newActorName := "Updated Name" + newContactInfo := "updated@example.com" + newLocation := "Updated Location" + + request := UpdateRequest{ + Name: &newActorName, + Contact: &newContactInfo, + Location: &newLocation, + } + + updated, err := handler.UpdateHandler(ctx, db, wallet.Address, request) + require.NoError(t, err) + require.NotNil(t, updated) + + require.Equal(t, newActorName, updated.ActorName) + require.Equal(t, newContactInfo, updated.ContactInfo) + require.Equal(t, newLocation, updated.Location) + + // Verify essential fields are unchanged + require.Equal(t, wallet.Address, updated.Address) + require.Equal(t, wallet.PrivateKey, updated.PrivateKey) + require.Equal(t, wallet.ActorID, updated.ActorID) + require.Equal(t, wallet.WalletType, updated.WalletType) + }) + + t.Run("update single field", func(t *testing.T) { + newActorName := "Single Field Update" + request := UpdateRequest{ + Name: &newActorName, + } + + updated, err := handler.UpdateHandler(ctx, db, wallet.Address, request) + require.NoError(t, err) + require.NotNil(t, updated) + + require.Equal(t, newActorName, updated.ActorName) + // Other fields should remain as they were from the previous update + require.Equal(t, "updated@example.com", updated.ContactInfo) + require.Equal(t, "Updated Location", updated.Location) + require.Equal(t, model.SPWallet, updated.WalletType) + }) + + t.Run("update with empty string", func(t *testing.T) { + emptyString := "" + request := UpdateRequest{ + Contact: &emptyString, + } + + updated, err := handler.UpdateHandler(ctx, db, wallet.Address, request) + require.NoError(t, err) + require.NotNil(t, updated) + + require.Equal(t, "", updated.ContactInfo) + }) + + t.Run("no fields to update", func(t *testing.T) { + request := UpdateRequest{} + + updated, err := handler.UpdateHandler(ctx, db, wallet.Address, request) + require.NoError(t, err) + require.NotNil(t, updated) + + // Should return the existing wallet unchanged + require.Equal(t, "Single Field Update", updated.ActorName) + }) + + t.Run("wallet not found", func(t *testing.T) { + request := UpdateRequest{ + Name: ptr.Of("Test"), + } + + updated, err := handler.UpdateHandler(ctx, db, "nonexistent-address", request) + require.Error(t, err) + require.Nil(t, updated) + require.Contains(t, err.Error(), "wallet not found") + }) + }) +} diff --git a/util/testutil/testutils.go b/util/testutil/testutils.go index aaba3023..5f8682aa 100644 --- a/util/testutil/testutils.go +++ b/util/testutil/testutils.go @@ -58,6 +58,8 @@ func GetFileTimestamp(t *testing.T, path string) int64 { var TestCid = cid.NewCidV1(cid.Raw, util.Hash([]byte("test"))) +const TestWalletActorID = "f0808055" + const TestWalletAddr = "f1fib3pv7jua2ockdugtz7viz3cyy6lkhh7rfx3sa" const TestPrivateKeyHex = "7b2254797065223a22736563703235366b31222c22507269766174654b6579223a226b35507976337148327349586343595a58594f5775453149326e32554539436861556b6c4e36695a5763453d227d"