diff --git a/.github/workflows/auto-generate.yml b/.github/workflows/auto-generate.yml index 5758549a..0d0fbf7a 100644 --- a/.github/workflows/auto-generate.yml +++ b/.github/workflows/auto-generate.yml @@ -17,6 +17,10 @@ jobs: with: go-version-file: 'go.mod' + - name: Build singularity CLI + run: | + go build -o singularity . + - name: Initialize DB run: | singularity admin init diff --git a/README.md b/README.md index 56bb2f5a..34caff3f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ sudo mv singularity-linux-amd64 /usr/local/bin/singularity # Or build from source git clone https://github.com/data-preservation-programs/singularity.git cd singularity -go build -o singularity . +go build -o singularity singularity.go ``` ### Basic Usage diff --git a/api/api.go b/api/api.go index bcb59fd0..a04f1afd 100644 --- a/api/api.go +++ b/api/api.go @@ -526,6 +526,7 @@ 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.GET("/api/wallet/:address/balance", s.toEchoHandler(s.walletHandler.GetBalanceHandler)) 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)) diff --git a/client/swagger/http/wallet/get_wallet_balance_parameters.go b/client/swagger/http/wallet/get_wallet_balance_parameters.go new file mode 100644 index 00000000..25bb9343 --- /dev/null +++ b/client/swagger/http/wallet/get_wallet_balance_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" +) + +// NewGetWalletBalanceParams creates a new GetWalletBalanceParams 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 NewGetWalletBalanceParams() *GetWalletBalanceParams { + return &GetWalletBalanceParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewGetWalletBalanceParamsWithTimeout creates a new GetWalletBalanceParams object +// with the ability to set a timeout on a request. +func NewGetWalletBalanceParamsWithTimeout(timeout time.Duration) *GetWalletBalanceParams { + return &GetWalletBalanceParams{ + timeout: timeout, + } +} + +// NewGetWalletBalanceParamsWithContext creates a new GetWalletBalanceParams object +// with the ability to set a context for a request. +func NewGetWalletBalanceParamsWithContext(ctx context.Context) *GetWalletBalanceParams { + return &GetWalletBalanceParams{ + Context: ctx, + } +} + +// NewGetWalletBalanceParamsWithHTTPClient creates a new GetWalletBalanceParams object +// with the ability to set a custom HTTPClient for a request. +func NewGetWalletBalanceParamsWithHTTPClient(client *http.Client) *GetWalletBalanceParams { + return &GetWalletBalanceParams{ + HTTPClient: client, + } +} + +/* +GetWalletBalanceParams contains all the parameters to send to the API endpoint + + for the get wallet balance operation. + + Typically these are written to a http.Request. +*/ +type GetWalletBalanceParams struct { + + /* Address. + + Wallet address + */ + Address string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the get wallet balance params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *GetWalletBalanceParams) WithDefaults() *GetWalletBalanceParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the get wallet balance params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *GetWalletBalanceParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the get wallet balance params +func (o *GetWalletBalanceParams) WithTimeout(timeout time.Duration) *GetWalletBalanceParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the get wallet balance params +func (o *GetWalletBalanceParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the get wallet balance params +func (o *GetWalletBalanceParams) WithContext(ctx context.Context) *GetWalletBalanceParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the get wallet balance params +func (o *GetWalletBalanceParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the get wallet balance params +func (o *GetWalletBalanceParams) WithHTTPClient(client *http.Client) *GetWalletBalanceParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the get wallet balance params +func (o *GetWalletBalanceParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithAddress adds the address to the get wallet balance params +func (o *GetWalletBalanceParams) WithAddress(address string) *GetWalletBalanceParams { + o.SetAddress(address) + return o +} + +// SetAddress adds the address to the get wallet balance params +func (o *GetWalletBalanceParams) SetAddress(address string) { + o.Address = address +} + +// WriteToRequest writes these params to a swagger request +func (o *GetWalletBalanceParams) 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/get_wallet_balance_responses.go b/client/swagger/http/wallet/get_wallet_balance_responses.go new file mode 100644 index 00000000..329f7054 --- /dev/null +++ b/client/swagger/http/wallet/get_wallet_balance_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" +) + +// GetWalletBalanceReader is a Reader for the GetWalletBalance structure. +type GetWalletBalanceReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *GetWalletBalanceReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewGetWalletBalanceOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewGetWalletBalanceBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 500: + result := NewGetWalletBalanceInternalServerError() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("[GET /wallet/{address}/balance] GetWalletBalance", response, response.Code()) + } +} + +// NewGetWalletBalanceOK creates a GetWalletBalanceOK with default headers values +func NewGetWalletBalanceOK() *GetWalletBalanceOK { + return &GetWalletBalanceOK{} +} + +/* +GetWalletBalanceOK describes a response with status code 200, with default header values. + +OK +*/ +type GetWalletBalanceOK struct { + Payload *models.WalletBalanceResponse +} + +// IsSuccess returns true when this get wallet balance o k response has a 2xx status code +func (o *GetWalletBalanceOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this get wallet balance o k response has a 3xx status code +func (o *GetWalletBalanceOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get wallet balance o k response has a 4xx status code +func (o *GetWalletBalanceOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this get wallet balance o k response has a 5xx status code +func (o *GetWalletBalanceOK) IsServerError() bool { + return false +} + +// IsCode returns true when this get wallet balance o k response a status code equal to that given +func (o *GetWalletBalanceOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the get wallet balance o k response +func (o *GetWalletBalanceOK) Code() int { + return 200 +} + +func (o *GetWalletBalanceOK) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /wallet/{address}/balance][%d] getWalletBalanceOK %s", 200, payload) +} + +func (o *GetWalletBalanceOK) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /wallet/{address}/balance][%d] getWalletBalanceOK %s", 200, payload) +} + +func (o *GetWalletBalanceOK) GetPayload() *models.WalletBalanceResponse { + return o.Payload +} + +func (o *GetWalletBalanceOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.WalletBalanceResponse) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewGetWalletBalanceBadRequest creates a GetWalletBalanceBadRequest with default headers values +func NewGetWalletBalanceBadRequest() *GetWalletBalanceBadRequest { + return &GetWalletBalanceBadRequest{} +} + +/* +GetWalletBalanceBadRequest describes a response with status code 400, with default header values. + +Bad Request +*/ +type GetWalletBalanceBadRequest struct { + Payload *models.APIHTTPError +} + +// IsSuccess returns true when this get wallet balance bad request response has a 2xx status code +func (o *GetWalletBalanceBadRequest) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this get wallet balance bad request response has a 3xx status code +func (o *GetWalletBalanceBadRequest) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get wallet balance bad request response has a 4xx status code +func (o *GetWalletBalanceBadRequest) IsClientError() bool { + return true +} + +// IsServerError returns true when this get wallet balance bad request response has a 5xx status code +func (o *GetWalletBalanceBadRequest) IsServerError() bool { + return false +} + +// IsCode returns true when this get wallet balance bad request response a status code equal to that given +func (o *GetWalletBalanceBadRequest) IsCode(code int) bool { + return code == 400 +} + +// Code gets the status code for the get wallet balance bad request response +func (o *GetWalletBalanceBadRequest) Code() int { + return 400 +} + +func (o *GetWalletBalanceBadRequest) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /wallet/{address}/balance][%d] getWalletBalanceBadRequest %s", 400, payload) +} + +func (o *GetWalletBalanceBadRequest) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /wallet/{address}/balance][%d] getWalletBalanceBadRequest %s", 400, payload) +} + +func (o *GetWalletBalanceBadRequest) GetPayload() *models.APIHTTPError { + return o.Payload +} + +func (o *GetWalletBalanceBadRequest) 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 +} + +// NewGetWalletBalanceInternalServerError creates a GetWalletBalanceInternalServerError with default headers values +func NewGetWalletBalanceInternalServerError() *GetWalletBalanceInternalServerError { + return &GetWalletBalanceInternalServerError{} +} + +/* +GetWalletBalanceInternalServerError describes a response with status code 500, with default header values. + +Internal Server Error +*/ +type GetWalletBalanceInternalServerError struct { + Payload *models.APIHTTPError +} + +// IsSuccess returns true when this get wallet balance internal server error response has a 2xx status code +func (o *GetWalletBalanceInternalServerError) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this get wallet balance internal server error response has a 3xx status code +func (o *GetWalletBalanceInternalServerError) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get wallet balance internal server error response has a 4xx status code +func (o *GetWalletBalanceInternalServerError) IsClientError() bool { + return false +} + +// IsServerError returns true when this get wallet balance internal server error response has a 5xx status code +func (o *GetWalletBalanceInternalServerError) IsServerError() bool { + return true +} + +// IsCode returns true when this get wallet balance internal server error response a status code equal to that given +func (o *GetWalletBalanceInternalServerError) IsCode(code int) bool { + return code == 500 +} + +// Code gets the status code for the get wallet balance internal server error response +func (o *GetWalletBalanceInternalServerError) Code() int { + return 500 +} + +func (o *GetWalletBalanceInternalServerError) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /wallet/{address}/balance][%d] getWalletBalanceInternalServerError %s", 500, payload) +} + +func (o *GetWalletBalanceInternalServerError) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /wallet/{address}/balance][%d] getWalletBalanceInternalServerError %s", 500, payload) +} + +func (o *GetWalletBalanceInternalServerError) GetPayload() *models.APIHTTPError { + return o.Payload +} + +func (o *GetWalletBalanceInternalServerError) 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 3c5a425e..58bbd2f2 100644 --- a/client/swagger/http/wallet/wallet_client.go +++ b/client/swagger/http/wallet/wallet_client.go @@ -58,6 +58,8 @@ type ClientOption func(*runtime.ClientOperation) type ClientService interface { CreateWallet(params *CreateWalletParams, opts ...ClientOption) (*CreateWalletOK, error) + GetWalletBalance(params *GetWalletBalanceParams, opts ...ClientOption) (*GetWalletBalanceOK, error) + ImportWallet(params *ImportWalletParams, opts ...ClientOption) (*ImportWalletOK, error) InitWallet(params *InitWalletParams, opts ...ClientOption) (*InitWalletOK, error) @@ -109,6 +111,46 @@ func (a *Client) CreateWallet(params *CreateWalletParams, opts ...ClientOption) panic(msg) } +/* +GetWalletBalance gets wallet f i l and f i l balance + +Retrieves the FIL balance and FIL+ datacap balance for a specific wallet address +*/ +func (a *Client) GetWalletBalance(params *GetWalletBalanceParams, opts ...ClientOption) (*GetWalletBalanceOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewGetWalletBalanceParams() + } + op := &runtime.ClientOperation{ + ID: "GetWalletBalance", + Method: "GET", + PathPattern: "/wallet/{address}/balance", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &GetWalletBalanceReader{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.(*GetWalletBalanceOK) + 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 GetWalletBalance: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + /* ImportWallet imports a private key */ diff --git a/client/swagger/models/dataprep_create_request.go b/client/swagger/models/dataprep_create_request.go index 6e593bbf..78c8a610 100644 --- a/client/swagger/models/dataprep_create_request.go +++ b/client/swagger/models/dataprep_create_request.go @@ -7,7 +7,6 @@ package models import ( "context" - "fmt" "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" @@ -20,79 +19,79 @@ import ( // swagger:model dataprep.CreateRequest type DataprepCreateRequest struct { - // AutoCreateDeals - When true, automatically creates deals for packed CAR files. Requires either dealProvider or dealTemplate to be specified. + // Auto-deal creation parameters AutoCreateDeals *bool `json:"autoCreateDeals,omitempty"` - // DealAnnounceToIpni - Whether to announce deals to the InterPlanetary Network Indexer (IPNI) for content discovery + // Whether to announce to IPNI DealAnnounceToIpni *bool `json:"dealAnnounceToIpni,omitempty"` - // DealDuration - Deal duration in epochs (2880 epochs = 1 day, max 1555200 = 540 days). Required when autoCreateDeals is true and not using template. + // Deal duration DealDuration int64 `json:"dealDuration,omitempty"` - // DealHTTPHeaders - Custom HTTP headers to include when making deal proposals (key-value pairs) + // HTTP headers for deals DealHTTPHeaders struct { ModelConfigMap } `json:"dealHttpHeaders,omitempty"` - // DealKeepUnsealed - Whether to keep unsealed copy of the data with the storage provider + // Whether to keep unsealed copy DealKeepUnsealed *bool `json:"dealKeepUnsealed,omitempty"` - // DealPricePerDeal - Price in FIL per deal (flat rate regardless of size) + // Price in FIL per deal DealPricePerDeal float64 `json:"dealPricePerDeal,omitempty"` - // DealPricePerGb - Price in FIL per GiB of data + // Price in FIL per GiB DealPricePerGb float64 `json:"dealPricePerGb,omitempty"` - // DealPricePerGbEpoch - Price in FIL per GiB per epoch (time-based pricing) + // Price in FIL per GiB per epoch DealPricePerGbEpoch float64 `json:"dealPricePerGbEpoch,omitempty"` - // DealProvider - Storage Provider ID (e.g., f01234 or t01234). Required when autoCreateDeals is true and not using template. + // Storage Provider ID DealProvider string `json:"dealProvider,omitempty"` - // DealStartDelay - Delay before deal starts in epochs (0 to 141120 = 49 days) + // Deal start delay DealStartDelay int64 `json:"dealStartDelay,omitempty"` - // DealTemplate - Name or ID of a pre-configured deal template. When specified, template settings override individual deal parameters. + // Deal template name or ID to use (optional) DealTemplate string `json:"dealTemplate,omitempty"` - // DealURLTemplate - URL template for retrieving deal data (can include placeholders) + // URL template for deals DealURLTemplate string `json:"dealUrlTemplate,omitempty"` - // DealVerified - Whether deals should be verified deals (consumes DataCap) + // Whether deals should be verified DealVerified *bool `json:"dealVerified,omitempty"` - // DeleteAfterExport - Whether to delete source files after successful CAR export. Use with caution. + // Whether to delete the source files after export DeleteAfterExport *bool `json:"deleteAfterExport,omitempty"` - // MaxSize - Maximum size of CAR files (e.g., "32G", "1T"). Supports K/M/G/T/P suffixes. + // Maximum size of the CAR files to be created MaxSize *string `json:"maxSize,omitempty"` - // MinPieceSize - Minimum piece size for DAG and remainder pieces (e.g., "256", "1M"). Must be at least 256 bytes. + // Minimum piece size for the preparation, applies only to DAG and remainer pieces MinPieceSize *string `json:"minPieceSize,omitempty"` - // Name - Unique name for this data preparation job + // Name of the preparation // Required: true Name *string `json:"name"` - // NoDag - Disables folder DAG structure maintenance. Improves performance but folders won't have CIDs. + // Whether to disable maintaining folder dag structure for the sources. If disabled, DagGen will not be possible and folders will not have an associated CID. NoDag *bool `json:"noDag,omitempty"` - // NoInline - Disables inline storage. Saves database space but requires output storage configuration. + // Whether to disable inline storage for the preparation. Can save database space but requires at least one output storage. NoInline *bool `json:"noInline,omitempty"` - // OutputStorages - List of storage system names for CAR file output + // Name of Output storage systems to be used for the output OutputStorages []string `json:"outputStorages"` - // PieceSize - Target piece size for CAR files (e.g., "32G"). Must be power of 2 and at least 256 bytes. + // Target piece size of the CAR files used for piece commitment calculation PieceSize string `json:"pieceSize,omitempty"` - // SourceStorages - List of storage system names containing source data + // Name of Source storage systems to be used for the source SourceStorages []string `json:"sourceStorages"` - // SpValidation - Validates storage provider details before creating deals + // Enable storage provider validation SpValidation *bool `json:"spValidation,omitempty"` - // WalletValidation - Validates wallet balance before creating deals + // Enable wallet balance validation WalletValidation *bool `json:"walletValidation,omitempty"` } @@ -108,18 +107,6 @@ func (m *DataprepCreateRequest) Validate(formats strfmt.Registry) error { res = append(res, err) } - if err := m.validateDealFields(formats); err != nil { - res = append(res, err) - } - - if err := m.validateFieldDependencies(formats); err != nil { - res = append(res, err) - } - - if err := m.validateSizeFields(formats); err != nil { - res = append(res, err) - } - if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -143,112 +130,6 @@ func (m *DataprepCreateRequest) validateName(formats strfmt.Registry) error { return nil } -func (m *DataprepCreateRequest) validateDealFields(formats strfmt.Registry) error { - // Validate deal duration range (2880 epochs = 1 day to 1555200 epochs = 540 days) - if m.DealDuration != 0 && (m.DealDuration < 2880 || m.DealDuration > 1555200) { - return errors.New(400, "dealDuration must be between 2880 (1 day) and 1555200 (540 days) epochs") - } - - // Validate deal start delay (0 to 49 days in epochs) - if m.DealStartDelay < 0 || m.DealStartDelay > 141120 { - return errors.New(400, "dealStartDelay must be between 0 and 141120 (49 days) epochs") - } - - // Validate price fields are non-negative - if m.DealPricePerDeal < 0 { - return errors.New(400, "dealPricePerDeal must be non-negative") - } - if m.DealPricePerGb < 0 { - return errors.New(400, "dealPricePerGb must be non-negative") - } - if m.DealPricePerGbEpoch < 0 { - return errors.New(400, "dealPricePerGbEpoch must be non-negative") - } - - // Validate deal provider format if provided - if m.DealProvider != "" && !isValidActorID(m.DealProvider) { - return errors.New(400, "dealProvider must be a valid actor ID (e.g., f01234 or t01234)") - } - - return nil -} - -func (m *DataprepCreateRequest) validateFieldDependencies(formats strfmt.Registry) error { - // If auto-create deals is enabled, certain fields become required - if m.AutoCreateDeals != nil && *m.AutoCreateDeals { - if m.DealProvider == "" && m.DealTemplate == "" { - return errors.New(400, "when autoCreateDeals is true, either dealProvider or dealTemplate must be specified") - } - - // If using direct provider (not template), validate required fields - if m.DealProvider != "" && m.DealTemplate == "" { - if m.DealDuration == 0 { - return errors.New(400, "dealDuration is required when autoCreateDeals is true and using direct provider") - } - } - } - - // Validate HTTP headers - if len(m.DealHTTPHeaders.ModelConfigMap) > 0 { - for key, value := range m.DealHTTPHeaders.ModelConfigMap { - if key == "" { - return errors.New(400, "HTTP header keys cannot be empty") - } - if value == "" { - return errors.New(400, "HTTP header values cannot be empty") - } - // Validate header key format - if !isValidHTTPHeaderKey(key) { - return errors.New(400, fmt.Sprintf("invalid HTTP header key format: %s", key)) - } - } - } - - // URL template validation - if m.DealURLTemplate != "" { - if !isValidURLTemplate(m.DealURLTemplate) { - return errors.New(400, "dealUrlTemplate must be a valid URL template") - } - } - - return nil -} - -func (m *DataprepCreateRequest) validateSizeFields(formats strfmt.Registry) error { - // Validate max size if provided - if m.MaxSize != nil && *m.MaxSize != "" { - if _, err := parseSize(*m.MaxSize); err != nil { - return errors.New(400, fmt.Sprintf("invalid maxSize format: %v", err)) - } - } - - // Validate min piece size if provided - if m.MinPieceSize != nil && *m.MinPieceSize != "" { - size, err := parseSize(*m.MinPieceSize) - if err != nil { - return errors.New(400, fmt.Sprintf("invalid minPieceSize format: %v", err)) - } - // Must be at least 256 bytes - if size < 256 { - return errors.New(400, "minPieceSize must be at least 256 bytes") - } - } - - // Validate piece size if provided - if m.PieceSize != "" { - size, err := parseSize(m.PieceSize) - if err != nil { - return errors.New(400, fmt.Sprintf("invalid pieceSize format: %v", err)) - } - // Must be a power of 2 and at least 256 bytes - if !isPowerOfTwo(size) || size < 256 { - return errors.New(400, "pieceSize must be a power of 2 and at least 256 bytes") - } - } - - return nil -} - // ContextValidate validate this dataprep create request based on the context it is used func (m *DataprepCreateRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error { var res []error @@ -285,82 +166,3 @@ func (m *DataprepCreateRequest) UnmarshalBinary(b []byte) error { *m = res return nil } - -// Helper functions for validation - -func isValidActorID(id string) bool { - // Actor IDs must start with 'f' or 't' followed by numbers - if len(id) < 2 { - return false - } - if id[0] != 'f' && id[0] != 't' { - return false - } - for i := 1; i < len(id); i++ { - if id[i] < '0' || id[i] > '9' { - return false - } - } - return true -} - -func isValidHTTPHeaderKey(key string) bool { - // HTTP header keys should contain only alphanumeric characters, hyphens, and underscores - for _, ch := range key { - if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || - (ch >= '0' && ch <= '9') || ch == '-' || ch == '_') { - return false - } - } - return true -} - -func isValidURLTemplate(url string) bool { - // Basic URL template validation - should start with http:// or https:// - return len(url) > 7 && (url[:7] == "http://" || (len(url) > 8 && url[:8] == "https://")) -} - -func parseSize(s string) (int64, error) { - // Simple size parser - handles suffixes like K, M, G, T, P - if len(s) == 0 { - return 0, errors.New(400, "empty size string") - } - - multiplier := int64(1) - numStr := s - - if len(s) > 1 { - suffix := s[len(s)-1] - switch suffix { - case 'K', 'k': - multiplier = 1024 - numStr = s[:len(s)-1] - case 'M', 'm': - multiplier = 1024 * 1024 - numStr = s[:len(s)-1] - case 'G', 'g': - multiplier = 1024 * 1024 * 1024 - numStr = s[:len(s)-1] - case 'T', 't': - multiplier = 1024 * 1024 * 1024 * 1024 - numStr = s[:len(s)-1] - case 'P', 'p': - multiplier = 1024 * 1024 * 1024 * 1024 * 1024 - numStr = s[:len(s)-1] - } - } - - var num int64 - for _, ch := range numStr { - if ch < '0' || ch > '9' { - return 0, errors.New(400, fmt.Sprintf("invalid character in size: %c", ch)) - } - num = num*10 + int64(ch-'0') - } - - return num * multiplier, nil -} - -func isPowerOfTwo(n int64) bool { - return n > 0 && (n&(n-1)) == 0 -} diff --git a/client/swagger/models/model_deal_config.go b/client/swagger/models/model_deal_config.go index 037291f2..6dab3acd 100644 --- a/client/swagger/models/model_deal_config.go +++ b/client/swagger/models/model_deal_config.go @@ -7,12 +7,7 @@ package models import ( "context" - "fmt" - "net/url" - "regexp" - "strings" - "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" ) @@ -25,170 +20,78 @@ type ModelDealConfig struct { // AutoCreateDeals enables automatic deal creation after preparation completes AutoCreateDeals bool `json:"autoCreateDeals,omitempty"` - // DealAnnounceToIpni indicates whether to announce deals to the IPNI (InterPlanetary Network Indexer) + // DealAllowedPieceCIDs specifies which piece CIDs are allowed for this deal config + DealAllowedPieceCids []string `json:"dealAllowedPieceCids"` + + // DealAnnounceToIpni indicates whether to announce to IPNI DealAnnounceToIpni bool `json:"dealAnnounceToIpni,omitempty"` - // DealDuration specifies the deal duration in epochs (must be between 2880 and 1555200) - // Minimum: 2880 epochs (~24 hours), Maximum: 1555200 epochs (~540 days) + // DealDuration specifies the deal duration (time.Duration for backward compatibility) DealDuration int64 `json:"dealDuration,omitempty"` - // DealHTTPHeaders contains HTTP headers for deal requests - // Expected format: map[string]string with valid HTTP header keys and values + // DealForce indicates whether to force deal creation even if conditions aren't met + DealForce bool `json:"dealForce,omitempty"` + + // DealHTTPHeaders contains HTTP headers for deals DealHTTPHeaders interface{} `json:"dealHttpHeaders,omitempty"` - // DealKeepUnsealed indicates whether to keep unsealed copy of the data + // DealKeepUnsealed indicates whether to keep unsealed copy DealKeepUnsealed bool `json:"dealKeepUnsealed,omitempty"` - // DealPricePerDeal specifies the price in FIL per deal (must be non-negative) + // DealNotes provides additional notes or comments for the deal + DealNotes string `json:"dealNotes,omitempty"` + + // DealPricePerDeal specifies the price in FIL per deal DealPricePerDeal float64 `json:"dealPricePerDeal,omitempty"` - // DealPricePerGb specifies the price in FIL per GiB (must be non-negative) + // DealPricePerGb specifies the price in FIL per GiB DealPricePerGb float64 `json:"dealPricePerGb,omitempty"` - // DealPricePerGbEpoch specifies the price in FIL per GiB per epoch (must be non-negative) + // DealPricePerGbEpoch specifies the price in FIL per GiB per epoch DealPricePerGbEpoch float64 `json:"dealPricePerGbEpoch,omitempty"` // DealProvider specifies the Storage Provider ID for deals - // Must be a valid Filecoin actor ID (e.g., f01234 or t01234) DealProvider string `json:"dealProvider,omitempty"` - // DealStartDelay specifies the deal start delay in epochs (must be between 0 and 141120) - // Minimum: 0 epochs (immediate), Maximum: 141120 epochs (~49 days) + // DealStartDelay specifies the deal start delay (time.Duration for backward compatibility) DealStartDelay int64 `json:"dealStartDelay,omitempty"` // DealTemplate specifies the deal template name or ID to use (optional) DealTemplate string `json:"dealTemplate,omitempty"` - // DealURLTemplate specifies the URL template for retrieving deal data - // Must be a valid URL template with optional placeholders + // DealURLTemplate specifies the URL template for deals DealURLTemplate string `json:"dealUrlTemplate,omitempty"` - // DealVerified indicates whether deals should be verified deals + // DealVerified indicates whether deals should be verified DealVerified bool `json:"dealVerified,omitempty"` -} - -// Validate validates this model deal config -func (m *ModelDealConfig) Validate(formats strfmt.Registry) error { - var res []error - - if err := m.validateDealDuration(formats); err != nil { - res = append(res, err) - } - - if err := m.validateDealStartDelay(formats); err != nil { - res = append(res, err) - } - - if err := m.validatePrices(formats); err != nil { - res = append(res, err) - } - - if err := m.validateDealProvider(formats); err != nil { - res = append(res, err) - } - - if err := m.validateDealHTTPHeaders(formats); err != nil { - res = append(res, err) - } - - if err := m.validateDealURLTemplate(formats); err != nil { - res = append(res, err) - } - - if len(res) > 0 { - return errors.CompositeValidationError(res...) - } - return nil -} - -func (m *ModelDealConfig) validateDealDuration(formats strfmt.Registry) error { - if m.DealDuration == 0 { - return nil // Optional field - } - if m.DealDuration < 2880 || m.DealDuration > 1555200 { - return errors.New(400, fmt.Sprintf("deal duration must be between 2880 and 1555200 epochs, got %d", m.DealDuration)) - } - - return nil -} - -func (m *ModelDealConfig) validateDealStartDelay(formats strfmt.Registry) error { - if m.DealStartDelay < 0 || m.DealStartDelay > 141120 { - return errors.New(400, fmt.Sprintf("deal start delay must be between 0 and 141120 epochs, got %d", m.DealStartDelay)) - } - - return nil -} + // HTTP headers as string slice (matching deal schedule create command) + HTTPHeaders []string `json:"httpHeaders"` -func (m *ModelDealConfig) validatePrices(formats strfmt.Registry) error { - if m.DealPricePerDeal < 0 { - return errors.New(400, fmt.Sprintf("deal price per deal must be non-negative, got %f", m.DealPricePerDeal)) - } + // max pending deal number + MaxPendingDealNumber int64 `json:"maxPendingDealNumber,omitempty"` - if m.DealPricePerGb < 0 { - return errors.New(400, fmt.Sprintf("deal price per GiB must be non-negative, got %f", m.DealPricePerGb)) - } + // max pending deal size + MaxPendingDealSize string `json:"maxPendingDealSize,omitempty"` - if m.DealPricePerGbEpoch < 0 { - return errors.New(400, fmt.Sprintf("deal price per GiB per epoch must be non-negative, got %f", m.DealPricePerGbEpoch)) - } + // Scheduling fields (matching deal schedule create command) + ScheduleCron string `json:"scheduleCron,omitempty"` - return nil -} + // schedule deal number + ScheduleDealNumber int64 `json:"scheduleDealNumber,omitempty"` -func (m *ModelDealConfig) validateDealProvider(formats strfmt.Registry) error { - if m.DealProvider == "" { - return nil // Optional field - } + // schedule deal size + ScheduleDealSize string `json:"scheduleDealSize,omitempty"` - if !isValidDealProviderID(m.DealProvider) { - return errors.New(400, fmt.Sprintf("invalid storage provider ID format: %s (must be f01234 or t01234 format)", m.DealProvider)) - } + // Restriction fields (matching deal schedule create command) + TotalDealNumber int64 `json:"totalDealNumber,omitempty"` - return nil + // total deal size + TotalDealSize string `json:"totalDealSize,omitempty"` } -func (m *ModelDealConfig) validateDealHTTPHeaders(formats strfmt.Registry) error { - if m.DealHTTPHeaders == nil { - return nil // Optional field - } - - headers, ok := m.DealHTTPHeaders.(map[string]interface{}) - if !ok { - return errors.New(400, "HTTP headers must be a map[string]string") - } - - for key, value := range headers { - if !isValidDealHTTPHeaderKey(key) { - return errors.New(400, fmt.Sprintf("invalid HTTP header key: %s", key)) - } - - strValue, ok := value.(string) - if !ok { - return errors.New(400, fmt.Sprintf("HTTP header value must be a string for key: %s", key)) - } - - // Check for control characters in header value - for _, r := range strValue { - if r < 32 || r == 127 { - return errors.New(400, fmt.Sprintf("HTTP header value contains invalid control characters for key: %s", key)) - } - } - } - - return nil -} - -func (m *ModelDealConfig) validateDealURLTemplate(formats strfmt.Registry) error { - if m.DealURLTemplate == "" { - return nil // Optional field - } - - if !isValidDealURLTemplate(m.DealURLTemplate) { - return errors.New(400, fmt.Sprintf("invalid URL template: %s", m.DealURLTemplate)) - } - +// Validate validates this model deal config +func (m *ModelDealConfig) Validate(formats strfmt.Registry) error { return nil } @@ -214,87 +117,3 @@ func (m *ModelDealConfig) UnmarshalBinary(b []byte) error { *m = res return nil } - -// Helper functions for deal config validation - -// isValidDealProviderID validates Filecoin actor ID format (f01234 or t01234) -func isValidDealProviderID(id string) bool { - if len(id) < 2 { - return false - } - - // Check prefix - if id[0] != 'f' && id[0] != 't' { - return false - } - - // Check if it starts with f0 or t0 - if len(id) < 3 || id[1] != '0' { - return false - } - - // Check remaining characters are digits - for i := 2; i < len(id); i++ { - if id[i] < '0' || id[i] > '9' { - return false - } - } - - // Must have at least one digit after f0/t0 - return len(id) > 2 -} - -// isValidDealHTTPHeaderKey validates HTTP header key format -func isValidDealHTTPHeaderKey(key string) bool { - if key == "" { - return false - } - - // HTTP header field names consist of printable US-ASCII characters - // excluding separators - separators := "()<>@,;:\\\"/[]?={} \t" - - for _, r := range key { - // Must be printable ASCII (33-126) - if r < 33 || r > 126 { - return false - } - - // Must not be a separator - if strings.ContainsRune(separators, r) { - return false - } - } - - return true -} - -// isValidDealURLTemplate validates URL template format -func isValidDealURLTemplate(template string) bool { - if template == "" { - return false - } - - // Replace template placeholders with dummy values for validation - // Common placeholders: {piece_cid}, {data_cid}, {path}, etc. - placeholderRegex := regexp.MustCompile(`\{[^}]+\}`) - processedURL := placeholderRegex.ReplaceAllString(template, "placeholder") - - // Try to parse as URL - u, err := url.Parse(processedURL) - if err != nil { - return false - } - - // Must have a scheme (http or https) - if u.Scheme != "http" && u.Scheme != "https" { - return false - } - - // Must have a host - if u.Host == "" { - return false - } - - return true -} diff --git a/client/swagger/models/model_preparation.go b/client/swagger/models/model_preparation.go index 2c8a09e4..0ac857a6 100644 --- a/client/swagger/models/model_preparation.go +++ b/client/swagger/models/model_preparation.go @@ -7,10 +7,7 @@ package models import ( "context" - "fmt" - "net/url" "strconv" - "strings" "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" @@ -89,139 +86,15 @@ func (m *ModelPreparation) Validate(formats strfmt.Registry) error { res = append(res, err) } - if err := m.validateSPAndWalletFlags(); err != nil { - res = append(res, err) - } - - if err := m.validatePreparationConsistency(); err != nil { - res = append(res, err) - } - if len(res) > 0 { return errors.CompositeValidationError(res...) } return nil } -// validateURLTemplate validates that the URL template is properly formatted -func (m *ModelPreparation) validateURLTemplate(template string) error { - // Check if template contains required placeholders - if !strings.Contains(template, "{PIECE_CID}") { - return errors.New(400, "dealURLTemplate must contain {PIECE_CID} placeholder") - } - - // Try to parse the URL with a sample piece CID - sampleURL := strings.ReplaceAll(template, "{PIECE_CID}", "baga6ea4seaqbase32cid") - if _, err := url.Parse(sampleURL); err != nil { - return errors.New(400, fmt.Sprintf("dealURLTemplate is not a valid URL template: %v", err)) - } - - return nil -} - -// validateSPAndWalletFlags validates the SP and wallet validation flags -func (m *ModelPreparation) validateSPAndWalletFlags() error { - // If auto-create deals is enabled, validate that validation flags make sense - if m.DealConfig.AutoCreateDeals { - // SP validation is recommended when auto-creating deals - if !m.SpValidation { - // This is a warning, not an error - just log or handle as needed - // Could return a warning or just continue - } - - // Wallet validation is recommended for verified deals - if m.DealConfig.DealVerified && !m.WalletValidation { - // This is a warning, not an error - just log or handle as needed - // Could return a warning or just continue - } - } - - return nil -} - -// validatePreparationConsistency validates overall preparation consistency -func (m *ModelPreparation) validatePreparationConsistency() error { - // Validate piece size constraints - if m.MinPieceSize > 0 && m.PieceSize > 0 { - if m.MinPieceSize > m.PieceSize { - return errors.New(400, "minPieceSize cannot be greater than pieceSize") - } - } - - // Validate max size constraint - if m.MaxSize > 0 && m.PieceSize > 0 { - if m.MaxSize < m.PieceSize { - return errors.New(400, "maxSize cannot be less than pieceSize") - } - } - - // Validate storage requirements - if len(m.SourceStorages) == 0 { - return errors.New(400, "at least one source storage must be specified") - } - - if len(m.OutputStorages) == 0 { - return errors.New(400, "at least one output storage must be specified") - } - - return nil -} - func (m *ModelPreparation) validateDealConfig(formats strfmt.Registry) error { - // Check if both DealTemplateID and DealConfig are provided - if m.DealTemplateID > 0 && !swag.IsZero(m.DealConfig) { - // Check if any deal config fields are set when using a template - if m.DealConfig.AutoCreateDeals || - m.DealConfig.DealDuration > 0 || - m.DealConfig.DealStartDelay > 0 || - m.DealConfig.DealProvider != "" || - m.DealConfig.DealPricePerDeal > 0 || - m.DealConfig.DealPricePerGb > 0 || - m.DealConfig.DealPricePerGbEpoch > 0 || - m.DealConfig.DealURLTemplate != "" || - m.DealConfig.DealHTTPHeaders != nil { - return errors.New(400, "cannot specify both deal template and deal configuration fields") - } - } - - // If no deal template is specified and auto-create deals is enabled, validate required fields - if m.DealTemplateID == 0 && m.DealConfig.AutoCreateDeals { - // Validate required fields for auto deal creation - if m.DealConfig.DealProvider == "" { - return errors.Required("dealConfig.dealProvider", "body", nil) - } - - // Validate storage provider format (should start with 'f0' or 't0') - if !strings.HasPrefix(m.DealConfig.DealProvider, "f0") && !strings.HasPrefix(m.DealConfig.DealProvider, "t0") { - return errors.New(400, "dealProvider must be a valid storage provider ID (e.g., f01234 or t01234)") - } - - // Validate deal duration - if m.DealConfig.DealDuration <= 0 { - return errors.New(400, "dealDuration must be positive when auto-creating deals") - } - - // Validate deal start delay - if m.DealConfig.DealStartDelay < 0 { - return errors.New(400, "dealStartDelay cannot be negative") - } - - // Validate pricing - at least one pricing method should be specified - if m.DealConfig.DealPricePerDeal == 0 && m.DealConfig.DealPricePerGb == 0 && m.DealConfig.DealPricePerGbEpoch == 0 { - return errors.New(400, "at least one pricing method must be specified (dealPricePerDeal, dealPricePerGb, or dealPricePerGbEpoch)") - } - - // Validate URL template if provided - if m.DealConfig.DealURLTemplate != "" { - if err := m.validateURLTemplate(m.DealConfig.DealURLTemplate); err != nil { - return err - } - } - } - - // Call the embedded DealConfig validation if it exists - if err := m.DealConfig.ModelDealConfig.Validate(formats); err != nil { - return err + if swag.IsZero(m.DealConfig) { // not required + return nil } return nil diff --git a/client/swagger/models/wallet_balance_response.go b/client/swagger/models/wallet_balance_response.go new file mode 100644 index 00000000..aa4406a6 --- /dev/null +++ b/client/swagger/models/wallet_balance_response.go @@ -0,0 +1,65 @@ +// 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" +) + +// WalletBalanceResponse wallet balance response +// +// swagger:model wallet.BalanceResponse +type WalletBalanceResponse struct { + + // address + Address string `json:"address,omitempty"` + + // FIL balance in FIL units + Balance string `json:"balance,omitempty"` + + // Raw balance in attoFIL + BalanceAttoFil string `json:"balanceAttoFil,omitempty"` + + // FIL+ datacap balance + DataCap string `json:"dataCap,omitempty"` + + // Raw datacap in bytes + DataCapBytes int64 `json:"dataCapBytes,omitempty"` + + // Error message if any + Error string `json:"error,omitempty"` +} + +// Validate validates this wallet balance response +func (m *WalletBalanceResponse) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this wallet balance response based on context it is used +func (m *WalletBalanceResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *WalletBalanceResponse) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *WalletBalanceResponse) UnmarshalBinary(b []byte) error { + var res WalletBalanceResponse + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/cmd/app.go b/cmd/app.go index 08c9de4d..1b21bc4b 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -181,6 +181,7 @@ Upgrading: Category: "Operations", Usage: "Wallet management", Subcommands: []*cli.Command{ + wallet.BalanceCmd, wallet.CreateCmd, wallet.ImportCmd, wallet.InitCmd, diff --git a/cmd/wallet/balance.go b/cmd/wallet/balance.go new file mode 100644 index 00000000..9fa837f4 --- /dev/null +++ b/cmd/wallet/balance.go @@ -0,0 +1,48 @@ +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 BalanceCmd = &cli.Command{ + Name: "balance", + Usage: "Get wallet balance information", + ArgsUsage: "", + Description: `Get FIL balance and FIL+ datacap balance for a specific wallet address. +This command queries the Lotus network to retrieve current balance information. + +Examples: + singularity wallet balance f12syf7zd3lfsv43aj2kb454ymaqw7debhumjnbqa + singularity wallet balance --json f1abc123...def456 + +The command returns: +- FIL balance in human-readable format (e.g., "1.000000 FIL") +- Raw balance in attoFIL for precise calculations +- FIL+ datacap balance in GiB format (e.g., "1024.50 GiB") +- Raw datacap in bytes + +If there are issues retrieving either balance, partial results will be shown with error details.`, + Before: cliutil.CheckNArgs, + Action: func(c *cli.Context) error { + db, closer, err := database.OpenFromCLI(c) + if err != nil { + return errors.WithStack(err) + } + defer func() { _ = closer.Close() }() + + lotusClient := util.NewLotusClient(c.String("lotus-api"), c.String("lotus-token")) + + result, err := wallet.Default.GetBalanceHandler(c.Context, db, lotusClient, c.Args().Get(0)) + if err != nil { + return errors.WithStack(err) + } + + cliutil.Print(c, result) + return nil + }, +} diff --git a/database/util.go b/database/util.go index fd3ae43e..21e9d862 100644 --- a/database/util.go +++ b/database/util.go @@ -60,7 +60,10 @@ func (d *databaseLogger) Trace(ctx context.Context, begin time.Time, fc func() ( lvl = logging.LevelWarn sql = "[SLOW!] " + sql } - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) && !strings.Contains(err.Error(), sqlSerializationFailure) { + if ctx.Err() != nil { + lvl = logging.LevelError + logger.Errorw("context error in DB transaction", "ctxErr", ctx.Err(), "sql", sql) + } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) && !strings.Contains(err.Error(), sqlSerializationFailure) { lvl = logging.LevelError } diff --git a/docgen.sh b/docgen.sh index b539c233..d00aa9ef 100755 --- a/docgen.sh +++ b/docgen.sh @@ -1,3 +1,4 @@ +singularity admin init env USER='$USER' go run handler/storage/gen/main.go rm -rf docs/en/cli-reference env USER='$USER' go run docs/gen/clireference/main.go diff --git a/docs/en/SUMMARY.md b/docs/en/SUMMARY.md index 06630599..a1c082c2 100644 --- a/docs/en/SUMMARY.md +++ b/docs/en/SUMMARY.md @@ -61,11 +61,11 @@ * [Remove](cli-reference/deal/schedule/remove.md) * [Send Manual](cli-reference/deal/send-manual.md) * [List](cli-reference/deal/list.md) -* [Deal Template](cli-reference/deal-template/README.md) - * [Create](cli-reference/deal-template/create.md) - * [List](cli-reference/deal-template/list.md) - * [Get](cli-reference/deal-template/get.md) - * [Delete](cli-reference/deal-template/delete.md) +* [Deal Schedule Template](cli-reference/deal-schedule-template/README.md) + * [Create](cli-reference/deal-schedule-template/create.md) + * [List](cli-reference/deal-schedule-template/list.md) + * [Get](cli-reference/deal-schedule-template/get.md) + * [Delete](cli-reference/deal-schedule-template/delete.md) * [Run](cli-reference/run/README.md) * [Api](cli-reference/run/api.md) * [Dataset Worker](cli-reference/run/dataset-worker.md) @@ -75,6 +75,7 @@ * [Download Server](cli-reference/run/download-server.md) * [Unified](cli-reference/run/unified.md) * [Wallet](cli-reference/wallet/README.md) + * [Balance](cli-reference/wallet/balance.md) * [Create](cli-reference/wallet/create.md) * [Import](cli-reference/wallet/import.md) * [Init](cli-reference/wallet/init.md) diff --git a/docs/en/cli-reference/README.md b/docs/en/cli-reference/README.md index 51257ee8..f21f786f 100644 --- a/docs/en/cli-reference/README.md +++ b/docs/en/cli-reference/README.md @@ -47,12 +47,12 @@ COMMANDS: Daemons: run run different singularity components Operations: - admin Admin commands - deal Replication / Deal making management - deal-template Deal template management - wallet Wallet management - storage Create and manage storage system connections - prep Create and manage dataset preparations + admin Admin commands + deal Replication / Deal making management + deal-schedule-template, dst Deal schedule template management + wallet Wallet management + storage Create and manage storage system connections + prep Create and manage dataset preparations Utility: ez-prep Prepare a dataset from a local path download Download a CAR file from the metadata API diff --git a/docs/en/cli-reference/deal-schedule-template/README.md b/docs/en/cli-reference/deal-schedule-template/README.md new file mode 100644 index 00000000..a33ecc5d --- /dev/null +++ b/docs/en/cli-reference/deal-schedule-template/README.md @@ -0,0 +1,22 @@ +# Deal schedule template management + +{% code fullWidth="true" %} +``` +NAME: + singularity deal-schedule-template - Deal schedule template management + +USAGE: + singularity deal-schedule-template command [command options] + +COMMANDS: + create Create a new deal template with unified flags and defaults + help, h Shows a list of commands or help for one command + Deal Template Management: + list List all deal templates as pretty-printed JSON + get Get a deal template by ID or name + delete Delete a deal template by ID or name + +OPTIONS: + --help, -h show help +``` +{% endcode %} diff --git a/docs/en/cli-reference/deal-schedule-template/create.md b/docs/en/cli-reference/deal-schedule-template/create.md new file mode 100644 index 00000000..d6a43345 --- /dev/null +++ b/docs/en/cli-reference/deal-schedule-template/create.md @@ -0,0 +1,60 @@ +# Create a new deal template with unified flags and defaults + +{% code fullWidth="true" %} +``` +NAME: + singularity deal-schedule-template create - Create a new deal template with unified flags and defaults + +USAGE: + singularity deal-schedule-template create [command options] + +DESCRIPTION: + Create a new deal template using the same flags and default values as deal schedule create. + + Key flags: + --provider Storage Provider ID (e.g., f01234) + --duration Deal duration (default: 12840h) + --start-delay Deal start delay (default: 72h) + --verified Propose deals as verified (default: true) + --keep-unsealed Keep unsealed copy (default: true) + --ipni Announce deals to IPNI (default: true) + --http-header HTTP headers (key=value) + --allowed-piece-cid List of allowed piece CIDs + --allowed-piece-cid-file File with allowed piece CIDs + + See --help for all options. + +OPTIONS: + --allowed-piece-cid value [ --allowed-piece-cid value ] List of allowed piece CIDs for this template + --allowed-piece-cid-file value File containing list of allowed piece CIDs + --duration value Duration for storage deals (e.g., 12840h for 535 days) (default: 12840h0m0s) + --force Force deals regardless of replication restrictions (overrides max pending/total deal limits and piece CID restrictions) (default: false) + --help, -h show help + --http-header value [ --http-header value ] HTTP headers to be passed with the request (key=value format) + --ipni Whether to announce deals to IPNI (default: true) + --keep-unsealed Whether to keep unsealed copy of deals (default: true) + --name value Name of the deal template + --notes value Notes or tags for tracking purposes + --price-per-deal value Price in FIL per deal for storage deals (default: 0) + --price-per-gb value Price in FIL per GiB for storage deals (default: 0) + --price-per-gb-epoch value Price in FIL per GiB per epoch for storage deals (default: 0) + --provider value Storage Provider ID (e.g., f01000) + --start-delay value Start delay for storage deals (default: 72h0m0s) + --url-template value URL template for deals + --verified Whether deals should be verified (default: true) + + Restrictions + + --max-pending-deal-number value Max pending deal number overall (0 = unlimited) (default: 0) + --max-pending-deal-size value Max pending deal sizes overall (e.g., 1000GiB, 0 = unlimited) (default: "0") + --total-deal-number value Max total deal number for this template (0 = unlimited) (default: 0) + --total-deal-size value Max total deal sizes for this template (e.g., 100TiB, 0 = unlimited) (default: "0") + + Scheduling + + --schedule-cron value Cron schedule to send out batch deals (e.g., @daily, @hourly, '0 0 * * *') + --schedule-deal-number value Max deal number per triggered schedule (0 = unlimited) (default: 0) + --schedule-deal-size value Max deal sizes per triggered schedule (e.g., 500GiB, 0 = unlimited) (default: "0") + +``` +{% endcode %} diff --git a/docs/en/cli-reference/deal-schedule-template/delete.md b/docs/en/cli-reference/deal-schedule-template/delete.md new file mode 100644 index 00000000..6f372edf --- /dev/null +++ b/docs/en/cli-reference/deal-schedule-template/delete.md @@ -0,0 +1,18 @@ +# Delete a deal template by ID or name + +{% code fullWidth="true" %} +``` +NAME: + singularity deal-schedule-template delete - Delete a deal template by ID or name + +USAGE: + singularity deal-schedule-template delete [command options] + +CATEGORY: + Deal Template Management + +OPTIONS: + --force Force deletion without confirmation (default: false) + --help, -h show help +``` +{% endcode %} diff --git a/docs/en/cli-reference/deal-template/get.md b/docs/en/cli-reference/deal-schedule-template/get.md similarity index 52% rename from docs/en/cli-reference/deal-template/get.md rename to docs/en/cli-reference/deal-schedule-template/get.md index f3f11d6d..fe938f75 100644 --- a/docs/en/cli-reference/deal-template/get.md +++ b/docs/en/cli-reference/deal-schedule-template/get.md @@ -3,10 +3,10 @@ {% code fullWidth="true" %} ``` NAME: - singularity deal-template get - Get a deal template by ID or name + singularity deal-schedule-template get - Get a deal template by ID or name USAGE: - singularity deal-template get [command options] + singularity deal-schedule-template get [command options] CATEGORY: Deal Template Management diff --git a/docs/en/cli-reference/deal-schedule-template/list.md b/docs/en/cli-reference/deal-schedule-template/list.md new file mode 100644 index 00000000..d5029cc0 --- /dev/null +++ b/docs/en/cli-reference/deal-schedule-template/list.md @@ -0,0 +1,17 @@ +# List all deal templates as pretty-printed JSON + +{% code fullWidth="true" %} +``` +NAME: + singularity deal-schedule-template list - List all deal templates as pretty-printed JSON + +USAGE: + singularity deal-schedule-template list [command options] + +CATEGORY: + Deal Template Management + +OPTIONS: + --help, -h show help +``` +{% endcode %} diff --git a/docs/en/cli-reference/deal-template/README.md b/docs/en/cli-reference/deal-template/README.md deleted file mode 100644 index c00d1b34..00000000 --- a/docs/en/cli-reference/deal-template/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Deal template management - -{% code fullWidth="true" %} -``` -NAME: - singularity deal-template - Deal template management - -USAGE: - singularity deal-template command [command options] - -COMMANDS: - help, h Shows a list of commands or help for one command - Deal Template Management: - create Create a new deal template - list List all deal templates - get Get a deal template by ID or name - delete Delete a deal template by ID or name - -OPTIONS: - --help, -h show help -``` -{% endcode %} diff --git a/docs/en/cli-reference/deal-template/create.md b/docs/en/cli-reference/deal-template/create.md deleted file mode 100644 index d8346f98..00000000 --- a/docs/en/cli-reference/deal-template/create.md +++ /dev/null @@ -1,30 +0,0 @@ -# Create a new deal template - -{% code fullWidth="true" %} -``` -NAME: - singularity deal-template create - Create a new deal template - -USAGE: - singularity deal-template create [command options] - -CATEGORY: - Deal Template Management - -OPTIONS: - --name value Name of the deal template - --description value Description of the deal template - --deal-price-per-gb value Price in FIL per GiB for storage deals (default: 0) - --deal-price-per-gb-epoch value Price in FIL per GiB per epoch for storage deals (default: 0) - --deal-price-per-deal value Price in FIL per deal for storage deals (default: 0) - --deal-duration value Duration for storage deals (e.g., 535 days) (default: 0s) - --deal-start-delay value Start delay for storage deals (e.g., 72h) (default: 0s) - --deal-verified Whether deals should be verified (default: false) - --deal-keep-unsealed Whether to keep unsealed copy of deals (default: false) - --deal-announce-to-ipni Whether to announce deals to IPNI (default: false) - --deal-provider value Storage Provider ID for deals (e.g., f01000) - --deal-url-template value URL template for deals - --deal-http-headers value HTTP headers for deals in JSON format - --help, -h show help -``` -{% endcode %} diff --git a/docs/en/cli-reference/deal-template/delete.md b/docs/en/cli-reference/deal-template/delete.md deleted file mode 100644 index 74f58dae..00000000 --- a/docs/en/cli-reference/deal-template/delete.md +++ /dev/null @@ -1,17 +0,0 @@ -# Delete a deal template by ID or name - -{% code fullWidth="true" %} -``` -NAME: - singularity deal-template delete - Delete a deal template by ID or name - -USAGE: - singularity deal-template delete [command options] - -CATEGORY: - Deal Template Management - -OPTIONS: - --help, -h show help -``` -{% endcode %} diff --git a/docs/en/cli-reference/deal-template/list.md b/docs/en/cli-reference/deal-template/list.md deleted file mode 100644 index 70a681f9..00000000 --- a/docs/en/cli-reference/deal-template/list.md +++ /dev/null @@ -1,17 +0,0 @@ -# List all deal templates - -{% code fullWidth="true" %} -``` -NAME: - singularity deal-template list - List all deal templates - -USAGE: - singularity deal-template list [command options] - -CATEGORY: - Deal Template Management - -OPTIONS: - --help, -h show help -``` -{% endcode %} diff --git a/docs/en/cli-reference/deal/schedule/README.md b/docs/en/cli-reference/deal/schedule/README.md index 84be735a..0a4c4678 100644 --- a/docs/en/cli-reference/deal/schedule/README.md +++ b/docs/en/cli-reference/deal/schedule/README.md @@ -9,7 +9,7 @@ USAGE: singularity deal schedule command [command options] COMMANDS: - create Create a schedule to send out deals to a storage provider + create Create a schedule to send out deals to a storage provider with unified flags and defaults list List all deal making schedules update Update an existing schedule pause Pause a specific schedule diff --git a/docs/en/cli-reference/deal/schedule/create.md b/docs/en/cli-reference/deal/schedule/create.md index 7baae19c..e2afcd97 100644 --- a/docs/en/cli-reference/deal/schedule/create.md +++ b/docs/en/cli-reference/deal/schedule/create.md @@ -1,43 +1,28 @@ -# Create a schedule to send out deals to a storage provider +# Create a schedule to send out deals to a storage provider with unified flags and defaults {% code fullWidth="true" %} ``` NAME: - singularity deal schedule create - Create a schedule to send out deals to a storage provider + singularity deal schedule create - Create a schedule to send out deals to a storage provider with unified flags and defaults USAGE: singularity deal schedule create [command options] DESCRIPTION: - CRON pattern '--schedule-cron': The CRON pattern can either be a descriptor or a standard CRON pattern with optional second field - Standard CRON: - ┌───────────── minute (0 - 59) - │ ┌───────────── hour (0 - 23) - │ │ ┌───────────── day of the month (1 - 31) - │ │ │ ┌───────────── month (1 - 12) - │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday) - │ │ │ │ │ - │ │ │ │ │ - │ │ │ │ │ - * * * * * - - Optional Second field: - ┌───────────── second (0 - 59) - │ ┌───────────── minute (0 - 59) - │ │ ┌───────────── hour (0 - 23) - │ │ │ ┌───────────── day of the month (1 - 31) - │ │ │ │ ┌───────────── month (1 - 12) - │ │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday) - │ │ │ │ │ │ - │ │ │ │ │ │ - * * * * * * - - Descriptor: - @yearly, @annually - Equivalent to 0 0 1 1 * - @monthly - Equivalent to 0 0 1 * * - @weekly - Equivalent to 0 0 * * 0 - @daily, @midnight - Equivalent to 0 0 * * * - @hourly - Equivalent to 0 * * * * + Create a new deal schedule with unified flags and default values. + + Key flags: + --provider Storage Provider ID (e.g., f01234) + --duration Deal duration (default: 12840h) + --start-delay Deal start delay (default: 72h) + --verified Propose deals as verified (default: true) + --keep-unsealed Keep unsealed copy (default: true) + --ipni Announce deals to IPNI (default: true) + --http-header HTTP headers (key=value) + --allowed-piece-cid List of allowed piece CIDs + --allowed-piece-cid-file File with allowed piece CIDs + + See --help for all options. OPTIONS: --help, -h show help diff --git a/docs/en/cli-reference/onboard.md b/docs/en/cli-reference/onboard.md index 06826b28..5980454e 100644 --- a/docs/en/cli-reference/onboard.md +++ b/docs/en/cli-reference/onboard.md @@ -13,17 +13,13 @@ DESCRIPTION: It performs the following steps automatically: 1. Creates storage connections (if paths provided) - 2. Creates data preparation with deal parameters from a deal template + 2. Creates data preparation with deal template configuration 3. Starts scanning immediately 4. Enables automatic job progression (scan → pack → daggen → deals) 5. Optionally starts managed workers to process jobs This is the simplest way to onboard data from source to storage deals. - - IMPORTANT: Deal configuration is managed through deal templates (--deal-template-id). - Create a deal template first using 'singularity deal-template create' with your - desired deal parameters, then reference it during onboarding for consistent, - auditable deal configuration. + Use deal templates to configure deal parameters - individual deal flags are not supported. OPTIONS: --auto-create-deals Enable automatic deal creation after preparation completion (default: true) @@ -34,15 +30,15 @@ OPTIONS: --no-dag Disable maintaining folder DAG structure (default: false) --output value [ --output value ] Local output path(s) for CAR files (optional) --source value [ --source value ] Local source path(s) to onboard - --sp-validation Enable storage provider validation (default: false) + --sp-validation Enable storage provider validation (default: true) --start-workers Start managed workers to process jobs automatically (default: true) --timeout value Timeout for waiting for completion (0 = no timeout) (default: 0s) --wait-for-completion Wait and monitor until all jobs complete (default: false) - --wallet-validation Enable wallet balance validation (default: false) + --wallet-validation Enable wallet balance validation (default: true) Deal Settings - --deal-template-id value Deal template ID to use for deal configuration (required when auto-create-deals is enabled) + --deal-template-id value Deal template ID to use for deal configuration (required when auto-create-deals is enabled). Individual deal flags are not supported - use templates instead. ``` {% endcode %} diff --git a/docs/en/cli-reference/prep/create.md b/docs/en/cli-reference/prep/create.md index 8c838e05..ea8dd90a 100644 --- a/docs/en/cli-reference/prep/create.md +++ b/docs/en/cli-reference/prep/create.md @@ -49,8 +49,8 @@ OPTIONS: Validation - --sp-validation Enable storage provider validation before deal creation (default: false) - --wallet-validation Enable wallet balance validation before deal creation (default: false) + --sp-validation Enable storage provider validation before deal creation (default: true) + --wallet-validation Enable wallet balance validation before deal creation (default: true) Workflow Automation diff --git a/docs/en/cli-reference/wallet/README.md b/docs/en/cli-reference/wallet/README.md index 588a39ec..dc5baf3e 100644 --- a/docs/en/cli-reference/wallet/README.md +++ b/docs/en/cli-reference/wallet/README.md @@ -9,6 +9,7 @@ USAGE: singularity wallet command [command options] COMMANDS: + balance Get wallet balance information create Create a new wallet import Import a wallet from exported private key init Initialize a wallet diff --git a/docs/en/cli-reference/wallet/balance.md b/docs/en/cli-reference/wallet/balance.md new file mode 100644 index 00000000..8f146c65 --- /dev/null +++ b/docs/en/cli-reference/wallet/balance.md @@ -0,0 +1,30 @@ +# Get wallet balance information + +{% code fullWidth="true" %} +``` +NAME: + singularity wallet balance - Get wallet balance information + +USAGE: + singularity wallet balance [command options] + +DESCRIPTION: + Get FIL balance and FIL+ datacap balance for a specific wallet address. + This command queries the Lotus network to retrieve current balance information. + + Examples: + singularity wallet balance f12syf7zd3lfsv43aj2kb454ymaqw7debhumjnbqa + singularity wallet balance --json f1abc123...def456 + + The command returns: + - FIL balance in human-readable format (e.g., "1.000000 FIL") + - Raw balance in attoFIL for precise calculations + - FIL+ datacap balance in GiB format (e.g., "1024.50 GiB") + - Raw datacap in bytes + + If there are issues retrieving either balance, partial results will be shown with error details. + +OPTIONS: + --help, -h show help +``` +{% endcode %} diff --git a/docs/en/web-api-reference/wallet.md b/docs/en/web-api-reference/wallet.md index 89cb92c2..6dcf5f53 100644 --- a/docs/en/web-api-reference/wallet.md +++ b/docs/en/web-api-reference/wallet.md @@ -16,6 +16,10 @@ [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}/balance" method="get" %} +[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 %} diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 2de85cc6..481a85cc 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -5613,6 +5613,48 @@ const docTemplate = `{ } } }, + "/wallet/{address}/balance": { + "get": { + "description": "Retrieves the FIL balance and FIL+ datacap balance for a specific wallet address", + "produces": [ + "application/json" + ], + "tags": [ + "Wallet" + ], + "summary": "Get wallet FIL and FIL+ balance", + "operationId": "GetWalletBalance", + "parameters": [ + { + "type": "string", + "description": "Wallet address", + "name": "address", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/wallet.BalanceResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.HTTPError" + } + } + } + } + }, "/wallet/{address}/init": { "post": { "produces": [ @@ -6368,6 +6410,13 @@ const docTemplate = `{ "description": "AutoCreateDeals enables automatic deal creation after preparation completes", "type": "boolean" }, + "dealAllowedPieceCids": { + "description": "DealAllowedPieceCIDs specifies which piece CIDs are allowed for this deal config", + "type": "array", + "items": { + "type": "string" + } + }, "dealAnnounceToIpni": { "description": "DealAnnounceToIpni indicates whether to announce to IPNI", "type": "boolean" @@ -6376,6 +6425,10 @@ const docTemplate = `{ "description": "DealDuration specifies the deal duration (time.Duration for backward compatibility)", "type": "integer" }, + "dealForce": { + "description": "DealForce indicates whether to force deal creation even if conditions aren't met", + "type": "boolean" + }, "dealHttpHeaders": { "description": "DealHTTPHeaders contains HTTP headers for deals", "type": "object" @@ -6384,6 +6437,10 @@ const docTemplate = `{ "description": "DealKeepUnsealed indicates whether to keep unsealed copy", "type": "boolean" }, + "dealNotes": { + "description": "DealNotes provides additional notes or comments for the deal", + "type": "string" + }, "dealPricePerDeal": { "description": "DealPricePerDeal specifies the price in FIL per deal", "type": "number" @@ -6415,6 +6472,36 @@ const docTemplate = `{ "dealVerified": { "description": "DealVerified indicates whether deals should be verified", "type": "boolean" + }, + "httpHeaders": { + "description": "HTTP headers as string slice (matching deal schedule create command)", + "type": "array", + "items": { + "type": "string" + } + }, + "maxPendingDealNumber": { + "type": "integer" + }, + "maxPendingDealSize": { + "type": "string" + }, + "scheduleCron": { + "description": "Scheduling fields (matching deal schedule create command)", + "type": "string" + }, + "scheduleDealNumber": { + "type": "integer" + }, + "scheduleDealSize": { + "type": "string" + }, + "totalDealNumber": { + "description": "Restriction fields (matching deal schedule create command)", + "type": "integer" + }, + "totalDealSize": { + "type": "string" } } }, @@ -16743,6 +16830,34 @@ const docTemplate = `{ "store.PieceReader": { "type": "object" }, + "wallet.BalanceResponse": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "balance": { + "description": "FIL balance in FIL units", + "type": "string" + }, + "balanceAttoFil": { + "description": "Raw balance in attoFIL", + "type": "string" + }, + "dataCap": { + "description": "FIL+ datacap balance", + "type": "string" + }, + "dataCapBytes": { + "description": "Raw datacap in bytes", + "type": "integer" + }, + "error": { + "description": "Error message if any", + "type": "string" + } + } + }, "wallet.CreateRequest": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 208cda51..0d088f1d 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -5607,6 +5607,48 @@ } } }, + "/wallet/{address}/balance": { + "get": { + "description": "Retrieves the FIL balance and FIL+ datacap balance for a specific wallet address", + "produces": [ + "application/json" + ], + "tags": [ + "Wallet" + ], + "summary": "Get wallet FIL and FIL+ balance", + "operationId": "GetWalletBalance", + "parameters": [ + { + "type": "string", + "description": "Wallet address", + "name": "address", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/wallet.BalanceResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.HTTPError" + } + } + } + } + }, "/wallet/{address}/init": { "post": { "produces": [ @@ -6362,6 +6404,13 @@ "description": "AutoCreateDeals enables automatic deal creation after preparation completes", "type": "boolean" }, + "dealAllowedPieceCids": { + "description": "DealAllowedPieceCIDs specifies which piece CIDs are allowed for this deal config", + "type": "array", + "items": { + "type": "string" + } + }, "dealAnnounceToIpni": { "description": "DealAnnounceToIpni indicates whether to announce to IPNI", "type": "boolean" @@ -6370,6 +6419,10 @@ "description": "DealDuration specifies the deal duration (time.Duration for backward compatibility)", "type": "integer" }, + "dealForce": { + "description": "DealForce indicates whether to force deal creation even if conditions aren't met", + "type": "boolean" + }, "dealHttpHeaders": { "description": "DealHTTPHeaders contains HTTP headers for deals", "type": "object" @@ -6378,6 +6431,10 @@ "description": "DealKeepUnsealed indicates whether to keep unsealed copy", "type": "boolean" }, + "dealNotes": { + "description": "DealNotes provides additional notes or comments for the deal", + "type": "string" + }, "dealPricePerDeal": { "description": "DealPricePerDeal specifies the price in FIL per deal", "type": "number" @@ -6409,6 +6466,36 @@ "dealVerified": { "description": "DealVerified indicates whether deals should be verified", "type": "boolean" + }, + "httpHeaders": { + "description": "HTTP headers as string slice (matching deal schedule create command)", + "type": "array", + "items": { + "type": "string" + } + }, + "maxPendingDealNumber": { + "type": "integer" + }, + "maxPendingDealSize": { + "type": "string" + }, + "scheduleCron": { + "description": "Scheduling fields (matching deal schedule create command)", + "type": "string" + }, + "scheduleDealNumber": { + "type": "integer" + }, + "scheduleDealSize": { + "type": "string" + }, + "totalDealNumber": { + "description": "Restriction fields (matching deal schedule create command)", + "type": "integer" + }, + "totalDealSize": { + "type": "string" } } }, @@ -16737,6 +16824,34 @@ "store.PieceReader": { "type": "object" }, + "wallet.BalanceResponse": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "balance": { + "description": "FIL balance in FIL units", + "type": "string" + }, + "balanceAttoFil": { + "description": "Raw balance in attoFIL", + "type": "string" + }, + "dataCap": { + "description": "FIL+ datacap balance", + "type": "string" + }, + "dataCapBytes": { + "description": "Raw datacap in bytes", + "type": "integer" + }, + "error": { + "description": "Error message if any", + "type": "string" + } + } + }, "wallet.CreateRequest": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index dea9514c..ba147e37 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -477,6 +477,12 @@ definitions: description: AutoCreateDeals enables automatic deal creation after preparation completes type: boolean + dealAllowedPieceCids: + description: DealAllowedPieceCIDs specifies which piece CIDs are allowed for + this deal config + items: + type: string + type: array dealAnnounceToIpni: description: DealAnnounceToIpni indicates whether to announce to IPNI type: boolean @@ -484,12 +490,19 @@ definitions: description: DealDuration specifies the deal duration (time.Duration for backward compatibility) type: integer + dealForce: + description: DealForce indicates whether to force deal creation even if conditions + aren't met + type: boolean dealHttpHeaders: description: DealHTTPHeaders contains HTTP headers for deals type: object dealKeepUnsealed: description: DealKeepUnsealed indicates whether to keep unsealed copy type: boolean + dealNotes: + description: DealNotes provides additional notes or comments for the deal + type: string dealPricePerDeal: description: DealPricePerDeal specifies the price in FIL per deal type: number @@ -515,6 +528,27 @@ definitions: dealVerified: description: DealVerified indicates whether deals should be verified type: boolean + httpHeaders: + description: HTTP headers as string slice (matching deal schedule create command) + items: + type: string + type: array + maxPendingDealNumber: + type: integer + maxPendingDealSize: + type: string + scheduleCron: + description: Scheduling fields (matching deal schedule create command) + type: string + scheduleDealNumber: + type: integer + scheduleDealSize: + type: string + totalDealNumber: + description: Restriction fields (matching deal schedule create command) + type: integer + totalDealSize: + type: string type: object model.DealState: enum: @@ -8323,6 +8357,26 @@ definitions: type: object store.PieceReader: type: object + wallet.BalanceResponse: + properties: + address: + type: string + balance: + description: FIL balance in FIL units + type: string + balanceAttoFil: + description: Raw balance in attoFIL + type: string + dataCap: + description: FIL+ datacap balance + type: string + dataCapBytes: + description: Raw datacap in bytes + type: integer + error: + description: Error message if any + type: string + type: object wallet.CreateRequest: properties: actorId: @@ -12023,6 +12077,35 @@ paths: summary: Remove a wallet tags: - Wallet + /wallet/{address}/balance: + get: + description: Retrieves the FIL balance and FIL+ datacap balance for a specific + wallet address + operationId: GetWalletBalance + parameters: + - description: Wallet address + in: path + name: address + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/wallet.BalanceResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.HTTPError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.HTTPError' + summary: Get wallet FIL and FIL+ balance + tags: + - Wallet /wallet/{address}/init: post: operationId: InitWallet diff --git a/handler/wallet/balance.go b/handler/wallet/balance.go new file mode 100644 index 00000000..7094c32f --- /dev/null +++ b/handler/wallet/balance.go @@ -0,0 +1,160 @@ +package wallet + +import ( + "context" + "fmt" + "math/big" + "strconv" + + "github.com/cockroachdb/errors" + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + "github.com/ybbus/jsonrpc/v3" + "gorm.io/gorm" +) + +type BalanceResponse struct { + Address string `json:"address"` + Balance string `json:"balance"` // FIL balance in FIL units + BalanceAttoFIL string `json:"balanceAttoFil"` // Raw balance in attoFIL + DataCap string `json:"dataCap"` // FIL+ datacap balance + DataCapBytes int64 `json:"dataCapBytes"` // Raw datacap in bytes + Error *string `json:"error,omitempty"` // Error message if any +} + +// GetBalanceHandler retrieves the FIL and FIL+ balance for a specific wallet +// +// Parameters: +// - ctx: The context for the request +// - db: Database connection +// - lotusClient: Lotus JSON-RPC client +// - address: Wallet address to query +// +// Returns: +// - BalanceResponse containing balance information +// - Error if the operation fails +func (DefaultHandler) GetBalanceHandler( + ctx context.Context, + db *gorm.DB, + lotusClient jsonrpc.RPCClient, + walletAddress string, +) (*BalanceResponse, error) { + // Validate wallet address format + addr, err := address.NewFromString(walletAddress) + if err != nil { + return nil, errors.Wrapf(err, "invalid wallet address format: %s", walletAddress) + } + + // Initialize response + response := &BalanceResponse{ + Address: walletAddress, + Balance: "0", + DataCap: "0", + DataCapBytes: 0, + } + + // Get FIL balance using Lotus API + balance, err := getWalletBalance(ctx, lotusClient, addr) + if err != nil { + errMsg := fmt.Sprintf("failed to get wallet balance: %v", err) + response.Error = &errMsg + } else { + response.Balance = formatFILFromAttoFIL(balance.Int) + response.BalanceAttoFIL = balance.Int.String() + } + + // Get FIL+ datacap balance + datacap, err := getDatacapBalance(ctx, lotusClient, addr) + if err != nil { + // Always show datacap errors to help debug + if response.Error != nil { + errMsg := fmt.Sprintf("%s; failed to get datacap balance: %v", *response.Error, err) + response.Error = &errMsg + } else { + errMsg := fmt.Sprintf("failed to get datacap balance: %v", err) + response.Error = &errMsg + } + datacap = 0 + } + + response.DataCap = formatDatacap(datacap) + response.DataCapBytes = datacap + + return response, nil +} + +// getWalletBalance retrieves FIL balance from Lotus +func getWalletBalance(ctx context.Context, lotusClient jsonrpc.RPCClient, addr address.Address) (abi.TokenAmount, error) { + var balance string + // Pass address as string to avoid parameter marshaling issues + err := lotusClient.CallFor(ctx, &balance, "Filecoin.WalletBalance", addr.String()) + if err != nil { + return abi.TokenAmount{}, errors.WithStack(err) + } + + balanceInt, ok := new(big.Int).SetString(balance, 10) + if !ok { + return abi.TokenAmount{}, errors.New("failed to parse balance") + } + + return abi.TokenAmount{Int: balanceInt}, nil +} + +// getDatacapBalance retrieves FIL+ datacap balance from Lotus +func getDatacapBalance(ctx context.Context, lotusClient jsonrpc.RPCClient, addr address.Address) (int64, error) { + var result string + // Use Filecoin.StateVerifiedClientStatus to get datacap allocation + // This is the API method that corresponds to "lotus filplus check-client-datacap" + err := lotusClient.CallFor(ctx, &result, "Filecoin.StateVerifiedClientStatus", addr.String(), nil) + if err != nil { + return 0, errors.WithStack(err) + } + + // If result is empty or null, client has no datacap + if result == "" { + return 0, nil + } + + // Parse the datacap balance string + datacap, err := strconv.ParseInt(result, 10, 64) + if err != nil { + return 0, errors.WithStack(err) + } + + return datacap, nil +} + +// formatFILFromAttoFIL converts attoFIL to human-readable FIL +func formatFILFromAttoFIL(attoFIL *big.Int) string { + if attoFIL == nil { + return "0.000000 FIL" + } + + filValue := new(big.Float).SetInt(attoFIL) + filValue.Quo(filValue, big.NewFloat(1e18)) + + return filValue.Text('f', 6) + " FIL" // 6 decimal places with FIL unit +} + +// formatDatacap formats datacap bytes to human-readable format +func formatDatacap(bytes int64) string { + if bytes == 0 { + return "0.00 GiB" + } + + // Convert bytes to GiB for display + gib := float64(bytes) / (1024 * 1024 * 1024) + return fmt.Sprintf("%.2f GiB", gib) +} + +// @ID GetWalletBalance +// @Summary Get wallet FIL and FIL+ balance +// @Description Retrieves the FIL balance and FIL+ datacap balance for a specific wallet address +// @Tags Wallet +// @Param address path string true "Wallet address" +// @Produce json +// @Success 200 {object} BalanceResponse +// @Failure 400 {object} api.HTTPError +// @Failure 500 {object} api.HTTPError +// @Router /wallet/{address}/balance [get] +func _() {} diff --git a/handler/wallet/balance_test.go b/handler/wallet/balance_test.go new file mode 100644 index 00000000..b9b6a23f --- /dev/null +++ b/handler/wallet/balance_test.go @@ -0,0 +1,227 @@ +package wallet + +import ( + "context" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/ybbus/jsonrpc/v3" + "gorm.io/gorm" +) + +// MockLotusClient is a mock implementation of jsonrpc.RPCClient +type MockLotusClient struct { + mock.Mock +} + +func (m *MockLotusClient) Call(ctx context.Context, method string, params ...interface{}) (*jsonrpc.RPCResponse, error) { + args := m.Called(ctx, method, params) + return args.Get(0).(*jsonrpc.RPCResponse), args.Error(1) +} + +func (m *MockLotusClient) CallFor(ctx context.Context, out interface{}, method string, params ...interface{}) error { + args := m.Called(ctx, out, method, params) + return args.Error(0) +} + +func (m *MockLotusClient) CallBatch(ctx context.Context, requests jsonrpc.RPCRequests) (jsonrpc.RPCResponses, error) { + args := m.Called(ctx, requests) + return args.Get(0).(jsonrpc.RPCResponses), args.Error(1) +} + +func (m *MockLotusClient) CallBatchFor(ctx context.Context, out []interface{}, requests jsonrpc.RPCRequests) error { + args := m.Called(ctx, out, requests) + return args.Error(0) +} + +func (m *MockLotusClient) CallBatchRaw(ctx context.Context, requests jsonrpc.RPCRequests) (jsonrpc.RPCResponses, error) { + args := m.Called(ctx, requests) + return args.Get(0).(jsonrpc.RPCResponses), args.Error(1) +} + +func (m *MockLotusClient) CallRaw(ctx context.Context, request *jsonrpc.RPCRequest) (*jsonrpc.RPCResponse, error) { + args := m.Called(ctx, request) + return args.Get(0).(*jsonrpc.RPCResponse), args.Error(1) +} + +func TestGetBalanceHandler(t *testing.T) { + ctx := context.Background() + mockClient := new(MockLotusClient) + + // Test valid wallet address with balance and datacap + t.Run("Valid wallet with balance and datacap", func(t *testing.T) { + // Mock WalletBalance call - using mock.Anything for variadic parameters + mockClient.On("CallFor", ctx, mock.AnythingOfType("*string"), "Filecoin.WalletBalance", mock.Anything). + Return(nil).Run(func(args mock.Arguments) { + resultPtr := args.Get(1).(*string) + *resultPtr = "1000000000000000000" // 1 FIL in attoFIL + }) + + // Mock StateVerifiedClientStatus call - using mock.Anything for variadic parameters + mockClient.On("CallFor", ctx, mock.AnythingOfType("*string"), "Filecoin.StateVerifiedClientStatus", mock.Anything, mock.Anything). + Return(nil).Run(func(args mock.Arguments) { + resultPtr := args.Get(1).(*string) + *resultPtr = "1099511627776" // 1024 GiB in bytes + }) + + handler := DefaultHandler{} + result, err := handler.GetBalanceHandler(ctx, &gorm.DB{}, mockClient, "f12syf7zd3lfsv43aj2kb454ymaqw7debhumjnbqa") + + require.NoError(t, err) + assert.Equal(t, "f12syf7zd3lfsv43aj2kb454ymaqw7debhumjnbqa", result.Address) + assert.Equal(t, "1.000000 FIL", result.Balance) + assert.Equal(t, "1000000000000000000", result.BalanceAttoFIL) + assert.Equal(t, "1024.00 GiB", result.DataCap) + assert.Equal(t, int64(1099511627776), result.DataCapBytes) + + mockClient.AssertExpectations(t) + mockClient.ExpectedCalls = nil + }) + + // Test wallet with zero balance and no datacap + t.Run("Wallet with zero balance and no datacap", func(t *testing.T) { + // Mock WalletBalance call + mockClient.On("CallFor", ctx, mock.AnythingOfType("*string"), "Filecoin.WalletBalance", mock.Anything). + Return(nil).Run(func(args mock.Arguments) { + resultPtr := args.Get(1).(*string) + *resultPtr = "0" // 0 FIL + }) + + // Mock StateVerifiedClientStatus call returning no datacap + mockClient.On("CallFor", ctx, mock.AnythingOfType("*string"), "Filecoin.StateVerifiedClientStatus", mock.Anything, mock.Anything). + Return(nil).Run(func(args mock.Arguments) { + resultPtr := args.Get(1).(*string) + *resultPtr = "0" // No datacap + }) + + handler := DefaultHandler{} + result, err := handler.GetBalanceHandler(ctx, &gorm.DB{}, mockClient, "f12syf7zd3lfsv43aj2kb454ymaqw7debhumjnbqa") + + require.NoError(t, err) + assert.Equal(t, "f12syf7zd3lfsv43aj2kb454ymaqw7debhumjnbqa", result.Address) + assert.Equal(t, "0.000000 FIL", result.Balance) + assert.Equal(t, "0", result.BalanceAttoFIL) + assert.Equal(t, "0.00 GiB", result.DataCap) + assert.Equal(t, int64(0), result.DataCapBytes) + + mockClient.AssertExpectations(t) + mockClient.ExpectedCalls = nil + }) + + // Test invalid wallet address + t.Run("Invalid wallet address", func(t *testing.T) { + handler := DefaultHandler{} + _, err := handler.GetBalanceHandler(ctx, &gorm.DB{}, mockClient, "invalid-address") + + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid wallet address format") + }) + + // Test API error handling for balance lookup + t.Run("Balance API error", func(t *testing.T) { + mockClient.On("CallFor", ctx, mock.AnythingOfType("*string"), "Filecoin.WalletBalance", mock.Anything). + Return(assert.AnError) + + // Since the implementation still tries to get datacap even when balance fails, we need to mock this too + mockClient.On("CallFor", ctx, mock.AnythingOfType("*string"), "Filecoin.StateVerifiedClientStatus", mock.Anything, mock.Anything). + Return(nil).Run(func(args mock.Arguments) { + resultPtr := args.Get(1).(*string) + *resultPtr = "null" + }) + + handler := DefaultHandler{} + result, err := handler.GetBalanceHandler(ctx, &gorm.DB{}, mockClient, "f12syf7zd3lfsv43aj2kb454ymaqw7debhumjnbqa") + + require.NoError(t, err) // Function should not return error, but include error in response + assert.NotNil(t, result.Error) + assert.Contains(t, *result.Error, "failed to get wallet balance") + + // Should still have datacap info since that call succeeds + assert.Equal(t, "0.00 GiB", result.DataCap) + assert.Equal(t, int64(0), result.DataCapBytes) + + mockClient.AssertExpectations(t) + mockClient.ExpectedCalls = nil + }) + + // Test API error handling for datacap lookup + t.Run("Datacap API error", func(t *testing.T) { + // Mock successful balance call + mockClient.On("CallFor", ctx, mock.AnythingOfType("*string"), "Filecoin.WalletBalance", mock.Anything). + Return(nil).Run(func(args mock.Arguments) { + resultPtr := args.Get(1).(*string) + *resultPtr = "1000000000000000000" + }) + + // Mock failed datacap call + mockClient.On("CallFor", ctx, mock.AnythingOfType("*string"), "Filecoin.StateVerifiedClientStatus", mock.Anything, mock.Anything). + Return(assert.AnError) + + handler := DefaultHandler{} + result, err := handler.GetBalanceHandler(ctx, &gorm.DB{}, mockClient, "f12syf7zd3lfsv43aj2kb454ymaqw7debhumjnbqa") + + require.NoError(t, err) // Function should not return error, but include error in response + assert.NotNil(t, result.Error) + assert.Contains(t, *result.Error, "failed to get datacap balance") + + // Should still have balance info since that call succeeds + assert.Equal(t, "1.000000 FIL", result.Balance) + assert.Equal(t, "1000000000000000000", result.BalanceAttoFIL) + + // Datacap should be zero due to error + assert.Equal(t, "0.00 GiB", result.DataCap) + assert.Equal(t, int64(0), result.DataCapBytes) + + mockClient.AssertExpectations(t) + mockClient.ExpectedCalls = nil + }) +} + +func TestFormatFILFromAttoFIL(t *testing.T) { + testCases := []struct { + name string + input string + expected string + }{ + {"Zero balance", "0", "0.000000 FIL"}, + {"One FIL", "1000000000000000000", "1.000000 FIL"}, + {"Half FIL", "500000000000000000", "0.500000 FIL"}, + {"Small amount", "1000000000000000", "0.001000 FIL"}, + {"Large amount", "10000000000000000000", "10.000000 FIL"}, + {"Fractional amount", "1500000000000000000", "1.500000 FIL"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var amount big.Int + amount.SetString(tc.input, 10) + result := formatFILFromAttoFIL(&amount) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestFormatDatacap(t *testing.T) { + testCases := []struct { + name string + input int64 + expected string + }{ + {"Zero datacap", 0, "0.00 GiB"}, + {"One GiB", 1073741824, "1.00 GiB"}, + {"Half GiB", 536870912, "0.50 GiB"}, + {"Multiple GiB", 10737418240, "10.00 GiB"}, + {"Large datacap", 1099511627776, "1024.00 GiB"}, // 1 TiB + {"Fractional GiB", 1610612736, "1.50 GiB"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := formatDatacap(tc.input) + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/handler/wallet/import.go b/handler/wallet/import.go index e27c162c..db04007a 100644 --- a/handler/wallet/import.go +++ b/handler/wallet/import.go @@ -2,6 +2,7 @@ package wallet import ( "context" + "fmt" "github.com/cockroachdb/errors" "github.com/data-preservation-programs/singularity/database" @@ -64,7 +65,7 @@ func (DefaultHandler) ImportHandler( 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")) + return nil, fmt.Errorf("failed to lookup state for wallet address %s: %w", addr.String(), err) } _, err = address.NewFromString(result) diff --git a/handler/wallet/interface.go b/handler/wallet/interface.go index bc8f6329..c9361c8d 100644 --- a/handler/wallet/interface.go +++ b/handler/wallet/interface.go @@ -29,6 +29,12 @@ type Handler interface { preparation string, wallet string, ) (*model.Preparation, error) + GetBalanceHandler( + ctx context.Context, + db *gorm.DB, + lotusClient jsonrpc.RPCClient, + walletAddress string, + ) (*BalanceResponse, error) ImportHandler( ctx context.Context, db *gorm.DB, @@ -88,6 +94,11 @@ func (m *MockWallet) DetachHandler(ctx context.Context, db *gorm.DB, preparation return args.Get(0).(*model.Preparation), args.Error(1) } +func (m *MockWallet) GetBalanceHandler(ctx context.Context, db *gorm.DB, lotusClient jsonrpc.RPCClient, walletAddress string) (*BalanceResponse, error) { + args := m.Called(ctx, db, lotusClient, walletAddress) + return args.Get(0).(*BalanceResponse), args.Error(1) +} + func (m *MockWallet) ImportHandler(ctx context.Context, db *gorm.DB, lotusClient jsonrpc.RPCClient, request ImportRequest) (*model.Wallet, error) { args := m.Called(ctx, db, lotusClient, request) return args.Get(0).(*model.Wallet), args.Error(1) diff --git a/migrate/migrations/202507090915_add_not_null_defaults.go b/migrate/migrations/202507090915_add_not_null_defaults.go index f19ebdc7..18c4849a 100644 --- a/migrate/migrations/202507090915_add_not_null_defaults.go +++ b/migrate/migrations/202507090915_add_not_null_defaults.go @@ -39,97 +39,16 @@ func _202507090915_add_not_null_defaults() *gormigrate.Migration { return err } - // Add NOT NULL DEFAULT constraints for string fields - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN template_deal_provider SET NOT NULL`).Error - if err != nil { - return err - } - - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN template_deal_provider SET DEFAULT ''`).Error - if err != nil { - return err - } - - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN template_deal_template SET NOT NULL`).Error - if err != nil { - return err - } - - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN template_deal_template SET DEFAULT ''`).Error - if err != nil { - return err - } - - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN template_deal_http_headers SET NOT NULL`).Error - if err != nil { - return err - } - - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN template_deal_http_headers SET DEFAULT ''`).Error - if err != nil { - return err - } - - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN template_deal_url_template SET NOT NULL`).Error - if err != nil { - return err - } - - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN template_deal_url_template SET DEFAULT ''`).Error - if err != nil { - return err - } - - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN description SET NOT NULL`).Error - if err != nil { - return err - } - - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN description SET DEFAULT ''`).Error - if err != nil { - return err - } - - // Add NOT NULL constraint to name field (already unique, but should be explicit) - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN name SET NOT NULL`).Error - if err != nil { - return err - } + // Note: SQLite does not support ALTER COLUMN operations for adding NOT NULL constraints + // Since we've already cleaned up NULL values above, we'll skip the constraint modifications + // The application layer will handle validation and new tables created will have proper constraints return nil }, Rollback: func(db *gorm.DB) error { - // Remove NOT NULL constraints (making columns nullable again) - err := db.Exec(`ALTER TABLE deal_templates ALTER COLUMN template_deal_provider DROP NOT NULL`).Error - if err != nil { - return err - } - - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN template_deal_template DROP NOT NULL`).Error - if err != nil { - return err - } - - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN template_deal_http_headers DROP NOT NULL`).Error - if err != nil { - return err - } - - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN template_deal_url_template DROP NOT NULL`).Error - if err != nil { - return err - } - - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN description DROP NOT NULL`).Error - if err != nil { - return err - } - - err = db.Exec(`ALTER TABLE deal_templates ALTER COLUMN name DROP NOT NULL`).Error - if err != nil { - return err - } - + // Note: SQLite rollback for NOT NULL constraints would require table recreation + // Since we only cleaned up NULL values and didn't actually modify constraints, + // there's nothing to rollback for SQLite return nil }, } diff --git a/migrate/migrations/202507141000_add_deal_notes_to_preparations.go b/migrate/migrations/202507141000_add_deal_notes_to_preparations.go new file mode 100644 index 00000000..d673cb68 --- /dev/null +++ b/migrate/migrations/202507141000_add_deal_notes_to_preparations.go @@ -0,0 +1,141 @@ +package migrations + +import ( + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +// _202507141000_add_deal_notes_to_preparations adds the missing deal_config_deal_notes column to the preparations table +func _202507141000_add_deal_notes_to_preparations() *gormigrate.Migration { + return &gormigrate.Migration{ + ID: "202507141000_add_deal_notes_to_preparations", + Migrate: func(db *gorm.DB) error { + // Add the missing deal_config_deal_notes column to the preparations table + err := db.Exec(`ALTER TABLE preparations ADD COLUMN deal_config_deal_notes TEXT DEFAULT ''`).Error + if err != nil { + return err + } + + // Add the missing deal_config_deal_force column to the preparations table + err = db.Exec(`ALTER TABLE preparations ADD COLUMN deal_config_deal_force BOOLEAN DEFAULT false`).Error + if err != nil { + return err + } + + // Add the missing deal_config_deal_allowed_piece_cids column to the preparations table + err = db.Exec(`ALTER TABLE preparations ADD COLUMN deal_config_deal_allowed_piece_cids JSON DEFAULT '[]'`).Error + if err != nil { + return err + } + + // Add the missing deal_config_schedule_cron column to the preparations table + err = db.Exec(`ALTER TABLE preparations ADD COLUMN deal_config_schedule_cron VARCHAR(255) DEFAULT ''`).Error + if err != nil { + return err + } + + // Add the missing deal_config_schedule_deal_number column to the preparations table + err = db.Exec(`ALTER TABLE preparations ADD COLUMN deal_config_schedule_deal_number INTEGER DEFAULT 0`).Error + if err != nil { + return err + } + + // Add the missing deal_config_schedule_deal_size column to the preparations table + err = db.Exec(`ALTER TABLE preparations ADD COLUMN deal_config_schedule_deal_size VARCHAR(255) DEFAULT '0'`).Error + if err != nil { + return err + } + + // Add the missing deal_config_total_deal_number column to the preparations table + err = db.Exec(`ALTER TABLE preparations ADD COLUMN deal_config_total_deal_number INTEGER DEFAULT 0`).Error + if err != nil { + return err + } + + // Add the missing deal_config_total_deal_size column to the preparations table + err = db.Exec(`ALTER TABLE preparations ADD COLUMN deal_config_total_deal_size VARCHAR(255) DEFAULT '0'`).Error + if err != nil { + return err + } + + // Add the missing deal_config_max_pending_deal_number column to the preparations table + err = db.Exec(`ALTER TABLE preparations ADD COLUMN deal_config_max_pending_deal_number INTEGER DEFAULT 0`).Error + if err != nil { + return err + } + + // Add the missing deal_config_max_pending_deal_size column to the preparations table + err = db.Exec(`ALTER TABLE preparations ADD COLUMN deal_config_max_pending_deal_size VARCHAR(255) DEFAULT '0'`).Error + if err != nil { + return err + } + + // Add the missing deal_config_http_headers column to the preparations table + err = db.Exec(`ALTER TABLE preparations ADD COLUMN deal_config_http_headers JSON DEFAULT '[]'`).Error + if err != nil { + return err + } + + return nil + }, + Rollback: func(db *gorm.DB) error { + // Remove the added columns + err := db.Exec(`ALTER TABLE preparations DROP COLUMN deal_config_deal_notes`).Error + if err != nil { + return err + } + + err = db.Exec(`ALTER TABLE preparations DROP COLUMN deal_config_deal_force`).Error + if err != nil { + return err + } + + err = db.Exec(`ALTER TABLE preparations DROP COLUMN deal_config_deal_allowed_piece_cids`).Error + if err != nil { + return err + } + + err = db.Exec(`ALTER TABLE preparations DROP COLUMN deal_config_schedule_cron`).Error + if err != nil { + return err + } + + err = db.Exec(`ALTER TABLE preparations DROP COLUMN deal_config_schedule_deal_number`).Error + if err != nil { + return err + } + + err = db.Exec(`ALTER TABLE preparations DROP COLUMN deal_config_schedule_deal_size`).Error + if err != nil { + return err + } + + err = db.Exec(`ALTER TABLE preparations DROP COLUMN deal_config_total_deal_number`).Error + if err != nil { + return err + } + + err = db.Exec(`ALTER TABLE preparations DROP COLUMN deal_config_total_deal_size`).Error + if err != nil { + return err + } + + err = db.Exec(`ALTER TABLE preparations DROP COLUMN deal_config_max_pending_deal_number`).Error + if err != nil { + return err + } + + err = db.Exec(`ALTER TABLE preparations DROP COLUMN deal_config_max_pending_deal_size`).Error + if err != nil { + return err + } + + err = db.Exec(`ALTER TABLE preparations DROP COLUMN deal_config_http_headers`).Error + if err != nil { + return err + } + + return nil + }, + } +} diff --git a/migrate/migrations/migrations.go b/migrate/migrations/migrations.go index 934b33e2..e27314c0 100644 --- a/migrate/migrations/migrations.go +++ b/migrate/migrations/migrations.go @@ -14,5 +14,6 @@ func GetMigrations() []*gormigrate.Migration { _202507090900_add_missing_deal_template_fields(), _202507090915_add_not_null_defaults(), _202507091000_add_schedule_fields_to_deal_templates(), + _202507141000_add_deal_notes_to_preparations(), } }