From 1c3a1ce1fdb315463f67dc016317a951de6d549d Mon Sep 17 00:00:00 2001 From: anjor Date: Fri, 20 Feb 2026 22:15:49 +0000 Subject: [PATCH 1/6] Add explicit deal type support for schedules --- cmd/deal/schedule/create.go | 8 ++++++++ cmd/deal/schedule/update.go | 8 ++++++++ handler/deal/schedule/create.go | 10 ++++++++++ handler/deal/schedule/create_test.go | 16 ++++++++++++++++ handler/deal/schedule/update.go | 9 +++++++++ handler/deal/schedule/update_test.go | 16 ++++++++++++++++ model/replication.go | 1 + service/dealpusher/dealpusher.go | 6 ++++++ service/dealpusher/pdp_wiring_test.go | 5 +++++ 9 files changed, 79 insertions(+) diff --git a/cmd/deal/schedule/create.go b/cmd/deal/schedule/create.go index 37b3c331..29727590 100644 --- a/cmd/deal/schedule/create.go +++ b/cmd/deal/schedule/create.go @@ -9,6 +9,7 @@ import ( "github.com/data-preservation-programs/singularity/cmd/cliutil" "github.com/data-preservation-programs/singularity/database" "github.com/data-preservation-programs/singularity/handler/deal/schedule" + "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/util" "github.com/urfave/cli/v2" ) @@ -58,6 +59,12 @@ var CreateCmd = &cli.Command{ Usage: "Storage Provider ID to send deals to", Required: true, }, + &cli.StringFlag{ + Name: "deal-type", + Category: "Deal Proposal", + Usage: "Deal type: market (legacy f05) or pdp (f41)", + Value: string(model.DealTypeMarket), + }, &cli.StringSliceFlag{ Name: "http-header", Category: "Boost Only", @@ -219,6 +226,7 @@ var CreateCmd = &cli.Command{ request := schedule.CreateRequest{ Preparation: c.String("preparation"), Provider: c.String("provider"), + DealType: c.String("deal-type"), HTTPHeaders: c.StringSlice("http-header"), URLTemplate: c.String("url-template"), PricePerGBEpoch: c.Float64("price-per-gb-epoch"), diff --git a/cmd/deal/schedule/update.go b/cmd/deal/schedule/update.go index 64326e1e..89c569ad 100644 --- a/cmd/deal/schedule/update.go +++ b/cmd/deal/schedule/update.go @@ -83,6 +83,11 @@ var UpdateCmd = &cli.Command{ Usage: "Whether to propose deals as verified", Value: true, }, + &cli.StringFlag{ + Name: "deal-type", + Category: "Deal Proposal", + Usage: "Deal type: market (legacy f05) or pdp (f41)", + }, &cli.BoolFlag{ Name: "ipni", Category: "Boost Only", @@ -208,6 +213,9 @@ var UpdateCmd = &cli.Command{ if c.IsSet("verified") { request.Verified = ptr.Of(c.Bool("verified")) } + if c.IsSet("deal-type") { + request.DealType = ptr.Of(c.String("deal-type")) + } if c.IsSet("ipni") { request.IPNI = ptr.Of(c.Bool("ipni")) } diff --git a/handler/deal/schedule/create.go b/handler/deal/schedule/create.go index 6c5fc0ed..1932aecc 100644 --- a/handler/deal/schedule/create.go +++ b/handler/deal/schedule/create.go @@ -3,6 +3,7 @@ package schedule import ( "context" "net/url" + "slices" "strconv" "strings" "time" @@ -23,6 +24,7 @@ import ( type CreateRequest struct { Preparation string `json:"preparation" validation:"required"` // Preparation ID or name Provider string `json:"provider" validation:"required"` // Provider + DealType string `json:"dealType"` // Deal type: market (f05) or pdp (f41) HTTPHeaders []string `json:"httpHeaders"` // http headers to be passed with the request (i.e. key=value) URLTemplate string `json:"urlTemplate"` // URL template with PIECE_CID placeholder for boost to fetch the CAR file, i.e. http://127.0.0.1/piece/{PIECE_CID}.car PricePerGBEpoch float64 `default:"0" json:"pricePerGbEpoch"` // Price in FIL per GiB per epoch @@ -167,6 +169,13 @@ func (DefaultHandler) CreateHandler( if err != nil { return nil, errors.Join(handlererror.ErrInvalidParameter, errors.Wrapf(err, "provider %s cannot be resolved", request.Provider)) } + dealType := model.DealType(request.DealType) + if dealType == "" { + dealType = model.DealTypeMarket + } + if !slices.Contains(model.DealTypes, dealType) { + return nil, errors.Wrapf(handlererror.ErrInvalidParameter, "invalid deal type %q", request.DealType) + } headers := make(map[string]string) for _, header := range request.HTTPHeaders { @@ -205,6 +214,7 @@ func (DefaultHandler) CreateHandler( PricePerDeal: request.PricePerDeal, ScheduleCronPerpetual: request.ScheduleCronPerpetual, Force: request.Force, + DealType: dealType, } if err := database.DoRetry(ctx, func() error { diff --git a/handler/deal/schedule/create_test.go b/handler/deal/schedule/create_test.go index 7c3e8022..9c13a58e 100644 --- a/handler/deal/schedule/create_test.go +++ b/handler/deal/schedule/create_test.go @@ -53,6 +53,7 @@ func getMockLotusClient() jsonrpc.RPCClient { var createRequest = CreateRequest{ Preparation: "1", Provider: "f01000", + DealType: string(model.DealTypeMarket), HTTPHeaders: []string{"a=b"}, URLTemplate: "http://127.0.0.1", PricePerGBEpoch: 0, @@ -186,6 +187,20 @@ func TestCreateHandler_NoAssociatedWallet(t *testing.T) { }) } +func TestCreateHandler_InvalidDealType(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + err := db.Create(&model.Preparation{ + Wallets: []model.Wallet{{ID: "f01"}}, + }).Error + require.NoError(t, err) + badRequest := createRequest + badRequest.DealType = "unknown" + _, err = Default.CreateHandler(ctx, db, getMockLotusClient(), badRequest) + require.ErrorIs(t, err, handlererror.ErrInvalidParameter) + require.ErrorContains(t, err, "invalid deal type") + }) +} + func TestCreateHandler_InvalidProvider(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ @@ -260,6 +275,7 @@ func TestCreateHandler_Success(t *testing.T) { schedule, err := Default.CreateHandler(ctx, db, getMockLotusClient(), createRequest) require.NoError(t, err) require.NotNil(t, schedule) + require.Equal(t, model.DealTypeMarket, schedule.DealType) require.True(t, createRequest.Force) }) }) diff --git a/handler/deal/schedule/update.go b/handler/deal/schedule/update.go index bada5105..670c808f 100644 --- a/handler/deal/schedule/update.go +++ b/handler/deal/schedule/update.go @@ -3,6 +3,7 @@ package schedule import ( "context" "net/url" + "slices" "strings" "github.com/cockroachdb/errors" @@ -39,6 +40,7 @@ type UpdateRequest struct { //nolint:tagliatelle AllowedPieceCIDs []string `json:"allowedPieceCids"` // Allowed piece CIDs in this schedule Force *bool `json:"force"` // Force to send out deals regardless of replication restriction + DealType *string `json:"dealType"` // Deal type: market (f05) or pdp (f41) } // UpdateHandler modifies an existing schedule record based on the provided update request. @@ -235,6 +237,13 @@ func (DefaultHandler) UpdateHandler( if request.Force != nil { updates["force"] = *request.Force } + if request.DealType != nil { + dealType := model.DealType(*request.DealType) + if !slices.Contains(model.DealTypes, dealType) { + return nil, errors.Wrapf(handlererror.ErrInvalidParameter, "invalid deal type %q", *request.DealType) + } + updates["deal_type"] = dealType + } err = db.Model(&schedule).Updates(updates).Error if err != nil { diff --git a/handler/deal/schedule/update_test.go b/handler/deal/schedule/update_test.go index 2492ed1a..ff6a2f0d 100644 --- a/handler/deal/schedule/update_test.go +++ b/handler/deal/schedule/update_test.go @@ -34,6 +34,7 @@ var updateRequest = UpdateRequest{ AllowedPieceCIDs: []string{"baga6ea4seaqao7s73y24kcutaosvacpdjgfe5pw76ooefnyqw4ynr3d2y6x2mpq"}, ScheduleCronPerpetual: ptr.Of(true), Force: ptr.Of(true), + DealType: ptr.Of(string(model.DealTypeMarket)), } func TestUpdateHandler_DatasetNotFound(t *testing.T) { @@ -194,6 +195,20 @@ func TestUpdateHandler_InvalidAllowedPieceCID_NotCommp(t *testing.T) { }) } +func TestUpdateHandler_InvalidDealType(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + err := db.Create(&model.Schedule{ + Preparation: &model.Preparation{}, + }).Error + require.NoError(t, err) + badRequest := updateRequest + badRequest.DealType = ptr.Of("unknown") + _, err = Default.UpdateHandler(ctx, db, 1, badRequest) + require.ErrorIs(t, err, handlererror.ErrInvalidParameter) + require.ErrorContains(t, err, "invalid deal type") + }) +} + func TestUpdateHandler_Success(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Schedule{ @@ -204,6 +219,7 @@ func TestUpdateHandler_Success(t *testing.T) { require.NoError(t, err) require.NotNil(t, schedule) require.True(t, schedule.Force) + require.Equal(t, model.DealTypeMarket, schedule.DealType) }) } diff --git a/model/replication.go b/model/replication.go index 386fddb6..8a567aca 100644 --- a/model/replication.go +++ b/model/replication.go @@ -161,6 +161,7 @@ type Schedule struct { ErrorMessage string `json:"errorMessage" table:"verbose"` AllowedPieceCIDs StringSlice `gorm:"type:JSON;column:allowed_piece_cids" json:"allowedPieceCids" table:"verbose"` Force bool `json:"force"` + DealType DealType `gorm:"index;default:'market'" json:"dealType"` // Associations PreparationID PreparationID `json:"preparationId"` diff --git a/service/dealpusher/dealpusher.go b/service/dealpusher/dealpusher.go index ff9120b0..7a708617 100644 --- a/service/dealpusher/dealpusher.go +++ b/service/dealpusher/dealpusher.go @@ -452,6 +452,12 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) func (d *DealPusher) resolveScheduleDealType(schedule *model.Schedule) model.DealType { if d.scheduleDealTypeResolver == nil { + if schedule != nil { + switch schedule.DealType { + case model.DealTypeMarket, model.DealTypePDP: + return schedule.DealType + } + } return inferScheduleDealType(schedule) } return d.scheduleDealTypeResolver(schedule) diff --git a/service/dealpusher/pdp_wiring_test.go b/service/dealpusher/pdp_wiring_test.go index e22f8343..ccb18284 100644 --- a/service/dealpusher/pdp_wiring_test.go +++ b/service/dealpusher/pdp_wiring_test.go @@ -60,6 +60,11 @@ func TestDealPusher_ResolveScheduleDealType_DelegatedProviderInfersPDP(t *testin require.Equal(t, model.DealTypePDP, d.resolveScheduleDealType(&model.Schedule{Provider: providerAddr.String()})) } +func TestDealPusher_ResolveScheduleDealType_UsesScheduleDealType(t *testing.T) { + d := &DealPusher{} + require.Equal(t, model.DealTypePDP, d.resolveScheduleDealType(&model.Schedule{DealType: model.DealTypePDP})) +} + func TestDealPusher_RunSchedule_PDPWithoutDependenciesReturnsConfiguredError(t *testing.T) { d := &DealPusher{ scheduleDealTypeResolver: func(_ *model.Schedule) model.DealType { From f72808da0bb2cca232ba356f47e126e88082c98f Mon Sep 17 00:00:00 2001 From: anjor Date: Tue, 24 Feb 2026 16:36:33 +0000 Subject: [PATCH 2/6] Trigger CI after rebase onto main From 503cf77d20bc7fc8a132bc53a1bc100e9413fd88 Mon Sep 17 00:00:00 2001 From: anjor Date: Tue, 24 Feb 2026 16:49:13 +0000 Subject: [PATCH 3/6] Add generated swagger/docs outputs for schedule dealType --- client/swagger/models/model_schedule.go | 54 +++++++++++++++++++ .../swagger/models/schedule_create_request.go | 3 ++ .../swagger/models/schedule_update_request.go | 3 ++ docs/en/cli-reference/deal/schedule/create.md | 1 + docs/en/cli-reference/deal/schedule/update.md | 1 + docs/swagger/docs.go | 11 ++++ docs/swagger/swagger.json | 13 ++++- docs/swagger/swagger.yaml | 8 +++ 8 files changed, 93 insertions(+), 1 deletion(-) diff --git a/client/swagger/models/model_schedule.go b/client/swagger/models/model_schedule.go index 96038ee3..dfc7cc00 100644 --- a/client/swagger/models/model_schedule.go +++ b/client/swagger/models/model_schedule.go @@ -28,6 +28,9 @@ type ModelSchedule struct { // created at CreatedAt string `json:"createdAt,omitempty"` + // deal type + DealType ModelDealType `json:"dealType,omitempty"` + // duration Duration int64 `json:"duration,omitempty"` @@ -108,6 +111,10 @@ type ModelSchedule struct { func (m *ModelSchedule) Validate(formats strfmt.Registry) error { var res []error + if err := m.validateDealType(formats); err != nil { + res = append(res, err) + } + if err := m.validateHTTPHeaders(formats); err != nil { res = append(res, err) } @@ -122,6 +129,27 @@ func (m *ModelSchedule) Validate(formats strfmt.Registry) error { return nil } +func (m *ModelSchedule) validateDealType(formats strfmt.Registry) error { + if swag.IsZero(m.DealType) { // not required + return nil + } + + if err := m.DealType.Validate(formats); err != nil { + ve := new(errors.Validation) + if stderrors.As(err, &ve) { + return ve.ValidateName("dealType") + } + ce := new(errors.CompositeError) + if stderrors.As(err, &ce) { + return ce.ValidateName("dealType") + } + + return err + } + + return nil +} + func (m *ModelSchedule) validateHTTPHeaders(formats strfmt.Registry) error { if swag.IsZero(m.HTTPHeaders) { // not required return nil @@ -170,6 +198,10 @@ func (m *ModelSchedule) validateState(formats strfmt.Registry) error { func (m *ModelSchedule) ContextValidate(ctx context.Context, formats strfmt.Registry) error { var res []error + if err := m.contextValidateDealType(ctx, formats); err != nil { + res = append(res, err) + } + if err := m.contextValidateHTTPHeaders(ctx, formats); err != nil { res = append(res, err) } @@ -184,6 +216,28 @@ func (m *ModelSchedule) ContextValidate(ctx context.Context, formats strfmt.Regi return nil } +func (m *ModelSchedule) contextValidateDealType(ctx context.Context, formats strfmt.Registry) error { + + if swag.IsZero(m.DealType) { // not required + return nil + } + + if err := m.DealType.ContextValidate(ctx, formats); err != nil { + ve := new(errors.Validation) + if stderrors.As(err, &ve) { + return ve.ValidateName("dealType") + } + ce := new(errors.CompositeError) + if stderrors.As(err, &ce) { + return ce.ValidateName("dealType") + } + + return err + } + + return nil +} + func (m *ModelSchedule) contextValidateHTTPHeaders(ctx context.Context, formats strfmt.Registry) error { if swag.IsZero(m.HTTPHeaders) { // not required diff --git a/client/swagger/models/schedule_create_request.go b/client/swagger/models/schedule_create_request.go index 7e2ff099..4e264de4 100644 --- a/client/swagger/models/schedule_create_request.go +++ b/client/swagger/models/schedule_create_request.go @@ -20,6 +20,9 @@ type ScheduleCreateRequest struct { // Allowed piece CIDs in this schedule AllowedPieceCids []string `json:"allowedPieceCids"` + // Deal type: market (f05) or pdp (f41) + DealType string `json:"dealType,omitempty"` + // Duration in epoch or in duration format, i.e. 1500000, 2400h Duration *string `json:"duration,omitempty"` diff --git a/client/swagger/models/schedule_update_request.go b/client/swagger/models/schedule_update_request.go index 38158dd0..b5b7fb6d 100644 --- a/client/swagger/models/schedule_update_request.go +++ b/client/swagger/models/schedule_update_request.go @@ -20,6 +20,9 @@ type ScheduleUpdateRequest struct { // Allowed piece CIDs in this schedule AllowedPieceCids []string `json:"allowedPieceCids"` + // Deal type: market (f05) or pdp (f41) + DealType string `json:"dealType,omitempty"` + // Duration in epoch or in duration format, i.e. 1500000, 2400h Duration *string `json:"duration,omitempty"` diff --git a/docs/en/cli-reference/deal/schedule/create.md b/docs/en/cli-reference/deal/schedule/create.md index 7baae19c..7fae5fbb 100644 --- a/docs/en/cli-reference/deal/schedule/create.md +++ b/docs/en/cli-reference/deal/schedule/create.md @@ -52,6 +52,7 @@ OPTIONS: Deal Proposal + --deal-type value Deal type: market (legacy f05) or pdp (f41) (default: "market") --duration value, -d value Duration in epoch or in duration format, i.e. 1500000, 2400h (default: 12840h[535 days]) --keep-unsealed Whether to keep unsealed copy (default: true) --price-per-deal value Price in FIL per deal (default: 0) diff --git a/docs/en/cli-reference/deal/schedule/update.md b/docs/en/cli-reference/deal/schedule/update.md index a882c1c6..aefac99c 100644 --- a/docs/en/cli-reference/deal/schedule/update.md +++ b/docs/en/cli-reference/deal/schedule/update.md @@ -50,6 +50,7 @@ OPTIONS: Deal Proposal + --deal-type value Deal type: market (legacy f05) or pdp (f41) --duration value, -d value Duration in epoch or in duration format, i.e. 1500000, 2400h --keep-unsealed Whether to keep unsealed copy (default: true) --price-per-deal value Price in FIL per deal (default: 0) diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 52e9c221..f606be7f 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -6787,6 +6787,9 @@ const docTemplate = `{ "createdAt": { "type": "string" }, + "dealType": { + "$ref": "#/definitions/model.DealType" + }, "duration": { "type": "integer" }, @@ -6971,6 +6974,10 @@ const docTemplate = `{ "type": "string" } }, + "dealType": { + "description": "Deal type: market (f05) or pdp (f41)", + "type": "string" + }, "duration": { "description": "Duration in epoch or in duration format, i.e. 1500000, 2400h", "type": "string", @@ -7082,6 +7089,10 @@ const docTemplate = `{ "type": "string" } }, + "dealType": { + "description": "Deal type: market (f05) or pdp (f41)", + "type": "string" + }, "duration": { "description": "Duration in epoch or in duration format, i.e. 1500000, 2400h", "type": "string", diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index d8689f27..8d0b0d22 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -6780,6 +6780,9 @@ "createdAt": { "type": "string" }, + "dealType": { + "$ref": "#/definitions/model.DealType" + }, "duration": { "type": "integer" }, @@ -6964,6 +6967,10 @@ "type": "string" } }, + "dealType": { + "description": "Deal type: market (f05) or pdp (f41)", + "type": "string" + }, "duration": { "description": "Duration in epoch or in duration format, i.e. 1500000, 2400h", "type": "string", @@ -7075,6 +7082,10 @@ "type": "string" } }, + "dealType": { + "description": "Deal type: market (f05) or pdp (f41)", + "type": "string" + }, "duration": { "description": "Duration in epoch or in duration format, i.e. 1500000, 2400h", "type": "string", @@ -20350,4 +20361,4 @@ "description": "OpenAPI", "url": "https://swagger.io/resources/open-api/" } -} \ No newline at end of file +} diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index d9d366c9..16051174 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -612,6 +612,8 @@ definitions: type: boolean createdAt: type: string + dealType: + $ref: '#/definitions/model.DealType' duration: type: integer errorMessage: @@ -737,6 +739,9 @@ definitions: items: type: string type: array + dealType: + description: 'Deal type: market (f05) or pdp (f41)' + type: string duration: default: 12840h description: Duration in epoch or in duration format, i.e. 1500000, 2400h @@ -823,6 +828,9 @@ definitions: items: type: string type: array + dealType: + description: 'Deal type: market (f05) or pdp (f41)' + type: string duration: default: 12840h description: Duration in epoch or in duration format, i.e. 1500000, 2400h From 9e7d9927106d8799f91c1d43bc3c5ed341024111 Mon Sep 17 00:00:00 2001 From: anjor Date: Wed, 25 Feb 2026 13:22:18 +0000 Subject: [PATCH 4/6] Fix schedule create test wallet fixture --- handler/deal/schedule/create_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/handler/deal/schedule/create_test.go b/handler/deal/schedule/create_test.go index 9c13a58e..006fadea 100644 --- a/handler/deal/schedule/create_test.go +++ b/handler/deal/schedule/create_test.go @@ -190,7 +190,9 @@ func TestCreateHandler_NoAssociatedWallet(t *testing.T) { func TestCreateHandler_InvalidDealType(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Preparation{ - Wallets: []model.Wallet{{ID: "f01"}}, + Wallets: []model.Wallet{{ + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", + }}, }).Error require.NoError(t, err) badRequest := createRequest From d31e43347dad1d2673bbf1caca452c1b5f43c050 Mon Sep 17 00:00:00 2001 From: anjor Date: Wed, 25 Feb 2026 13:47:28 +0000 Subject: [PATCH 5/6] chore: retrigger CI for formatting check From 22b199f2820457fd270f51f4002d71a54302b57e Mon Sep 17 00:00:00 2001 From: anjor Date: Wed, 25 Feb 2026 13:54:56 +0000 Subject: [PATCH 6/6] Align swagger.json EOF with generated output --- docs/swagger/swagger.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 8d0b0d22..062909cd 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -20361,4 +20361,4 @@ "description": "OpenAPI", "url": "https://swagger.io/resources/open-api/" } -} +} \ No newline at end of file