diff --git a/client/swagger/models/deal_list_deal_request.go b/client/swagger/models/deal_list_deal_request.go index ef88917f..658d060d 100644 --- a/client/swagger/models/deal_list_deal_request.go +++ b/client/swagger/models/deal_list_deal_request.go @@ -20,7 +20,7 @@ import ( // swagger:model deal.ListDealRequest type DealListDealRequest struct { - // deal type filter (market for f05, pdp for f41) + // deal type filter (market/f05_paid for f05, pdp for f41, ddo for allocations) DealTypes []ModelDealType `json:"dealTypes"` // preparation ID or name filter diff --git a/client/swagger/models/model_deal.go b/client/swagger/models/model_deal.go index 71b6e8b3..856f0cc8 100644 --- a/client/swagger/models/model_deal.go +++ b/client/swagger/models/model_deal.go @@ -46,6 +46,15 @@ type ModelDeal struct { // error message ErrorMessage string `json:"errorMessage,omitempty"` + // F05 paid-deal fields (only populated for DealTypeF05Paid) + F05PaymentContract string `json:"f05PaymentContract,omitempty"` + + // f05 payment status + F05PaymentStatus string `json:"f05PaymentStatus,omitempty"` + + // f05 payment tx hash + F05PaymentTxHash string `json:"f05PaymentTxHash,omitempty"` + // id ID int64 `json:"id,omitempty"` diff --git a/client/swagger/models/model_deal_type.go b/client/swagger/models/model_deal_type.go index 4af0305a..85900575 100644 --- a/client/swagger/models/model_deal_type.go +++ b/client/swagger/models/model_deal_type.go @@ -33,6 +33,9 @@ const ( // ModelDealTypeMarket captures enum value "market" ModelDealTypeMarket ModelDealType = "market" + // ModelDealTypeF05Paid captures enum value "f05_paid" + ModelDealTypeF05Paid ModelDealType = "f05_paid" + // ModelDealTypePdp captures enum value "pdp" ModelDealTypePdp ModelDealType = "pdp" @@ -45,7 +48,7 @@ var modelDealTypeEnum []any func init() { var res []ModelDealType - if err := json.Unmarshal([]byte(`["market","pdp","ddo"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["market","f05_paid","pdp","ddo"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/client/swagger/models/schedule_create_request.go b/client/swagger/models/schedule_create_request.go index e6e7cf6e..ebbeaeb8 100644 --- a/client/swagger/models/schedule_create_request.go +++ b/client/swagger/models/schedule_create_request.go @@ -20,7 +20,7 @@ type ScheduleCreateRequest struct { // Allowed piece CIDs in this schedule AllowedPieceCids []string `json:"allowedPieceCids"` - // Deal type: market (f05), pdp (f41), or ddo + // Deal type: market (legacy f05), f05_paid (f05 with on-chain payments), pdp (f41), or ddo DealType string `json:"dealType,omitempty"` // Duration in epoch or in duration format, i.e. 1500000, 2400h diff --git a/client/swagger/models/schedule_update_request.go b/client/swagger/models/schedule_update_request.go index b5b7fb6d..c5f63d60 100644 --- a/client/swagger/models/schedule_update_request.go +++ b/client/swagger/models/schedule_update_request.go @@ -20,7 +20,7 @@ type ScheduleUpdateRequest struct { // Allowed piece CIDs in this schedule AllowedPieceCids []string `json:"allowedPieceCids"` - // Deal type: market (f05) or pdp (f41) + // Deal type: market (legacy f05), f05_paid (f05 with on-chain payments), pdp (f41), or ddo DealType string `json:"dealType,omitempty"` // Duration in epoch or in duration format, i.e. 1500000, 2400h diff --git a/cmd/deal/list.go b/cmd/deal/list.go index 394717eb..1d7d4a2a 100644 --- a/cmd/deal/list.go +++ b/cmd/deal/list.go @@ -36,7 +36,7 @@ var ListCmd = &cli.Command{ }, &cli.StringSliceFlag{ Name: "deal-type", - Usage: "Filter deals by type: market (legacy f05), pdp (f41 PDP deals)", + Usage: "Filter deals by type: market, f05_paid, pdp, ddo", }, }, Action: func(c *cli.Context) error { diff --git a/cmd/deal/schedule/create.go b/cmd/deal/schedule/create.go index 3bec5a13..07526380 100644 --- a/cmd/deal/schedule/create.go +++ b/cmd/deal/schedule/create.go @@ -62,7 +62,7 @@ var CreateCmd = &cli.Command{ &cli.StringFlag{ Name: "deal-type", Category: "Deal Proposal", - Usage: "Deal type: market (legacy f05), pdp (f41), or ddo (DDO allocations)", + Usage: "Deal type: market (legacy f05), f05_paid (f05 with on-chain payments), pdp (f41), or ddo (DDO allocations)", Value: string(model.DealTypeMarket), }, &cli.StringSliceFlag{ diff --git a/cmd/deal/schedule/update.go b/cmd/deal/schedule/update.go index 89c569ad..d9e75dee 100644 --- a/cmd/deal/schedule/update.go +++ b/cmd/deal/schedule/update.go @@ -86,7 +86,7 @@ var UpdateCmd = &cli.Command{ &cli.StringFlag{ Name: "deal-type", Category: "Deal Proposal", - Usage: "Deal type: market (legacy f05) or pdp (f41)", + Usage: "Deal type: market (legacy f05), f05_paid (f05 with on-chain payments), pdp (f41), or ddo (DDO allocations)", }, &cli.BoolFlag{ Name: "ipni", diff --git a/docs/en/cli-reference/deal/list.md b/docs/en/cli-reference/deal/list.md index 53a6f999..ecf94a0b 100644 --- a/docs/en/cli-reference/deal/list.md +++ b/docs/en/cli-reference/deal/list.md @@ -14,7 +14,7 @@ OPTIONS: --schedule value [ --schedule value ] Filter deals by schedule --provider value [ --provider value ] Filter deals by provider --state value [ --state value ] Filter deals by state: proposed, published, active, expired, proposal_expired, slashed - --deal-type value [ --deal-type value ] Filter deals by type: market (legacy f05), pdp (f41 PDP deals) + --deal-type value [ --deal-type value ] Filter deals by type: market, f05_paid, pdp, ddo --help, -h show help ``` {% endcode %} diff --git a/docs/en/cli-reference/deal/schedule/create.md b/docs/en/cli-reference/deal/schedule/create.md index f96f8a65..bc39659c 100644 --- a/docs/en/cli-reference/deal/schedule/create.md +++ b/docs/en/cli-reference/deal/schedule/create.md @@ -52,7 +52,7 @@ OPTIONS: Deal Proposal - --deal-type value Deal type: market (legacy f05), pdp (f41), or ddo (DDO allocations) (default: "market") + --deal-type value Deal type: market (legacy f05), f05_paid (f05 with on-chain payments), pdp (f41), or ddo (DDO allocations) (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 aefac99c..01f1064d 100644 --- a/docs/en/cli-reference/deal/schedule/update.md +++ b/docs/en/cli-reference/deal/schedule/update.md @@ -50,7 +50,7 @@ OPTIONS: Deal Proposal - --deal-type value Deal type: market (legacy f05) or pdp (f41) + --deal-type value Deal type: market (legacy f05), f05_paid (f05 with on-chain payments), pdp (f41), or ddo (DDO allocations) --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 3cddffd7..21ad78bc 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -6163,7 +6163,7 @@ const docTemplate = `{ "type": "object", "properties": { "dealTypes": { - "description": "deal type filter (market for f05, pdp for f41)", + "description": "deal type filter (market/f05_paid for f05, pdp for f41, ddo for allocations)", "type": "array", "items": { "$ref": "#/definitions/model.DealType" @@ -6515,6 +6515,16 @@ const docTemplate = `{ "errorMessage": { "type": "string" }, + "f05PaymentContract": { + "description": "F05 paid-deal fields (only populated for DealTypeF05Paid)", + "type": "string" + }, + "f05PaymentStatus": { + "type": "string" + }, + "f05PaymentTxHash": { + "type": "string" + }, "id": { "type": "integer" }, @@ -6603,11 +6613,13 @@ const docTemplate = `{ "type": "string", "enum": [ "market", + "f05_paid", "pdp", "ddo" ], "x-enum-varnames": [ "DealTypeMarket", + "DealTypeF05Paid", "DealTypePDP", "DealTypeDDO" ] @@ -6994,7 +7006,7 @@ const docTemplate = `{ } }, "dealType": { - "description": "Deal type: market (f05), pdp (f41), or ddo", + "description": "Deal type: market (legacy f05), f05_paid (f05 with on-chain payments), pdp (f41), or ddo", "type": "string" }, "duration": { @@ -7109,7 +7121,7 @@ const docTemplate = `{ } }, "dealType": { - "description": "Deal type: market (f05) or pdp (f41)", + "description": "Deal type: market (legacy f05), f05_paid (f05 with on-chain payments), pdp (f41), or ddo", "type": "string" }, "duration": { diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index adb43358..486bf63f 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -6156,7 +6156,7 @@ "type": "object", "properties": { "dealTypes": { - "description": "deal type filter (market for f05, pdp for f41)", + "description": "deal type filter (market/f05_paid for f05, pdp for f41, ddo for allocations)", "type": "array", "items": { "$ref": "#/definitions/model.DealType" @@ -6508,6 +6508,16 @@ "errorMessage": { "type": "string" }, + "f05PaymentContract": { + "description": "F05 paid-deal fields (only populated for DealTypeF05Paid)", + "type": "string" + }, + "f05PaymentStatus": { + "type": "string" + }, + "f05PaymentTxHash": { + "type": "string" + }, "id": { "type": "integer" }, @@ -6596,11 +6606,13 @@ "type": "string", "enum": [ "market", + "f05_paid", "pdp", "ddo" ], "x-enum-varnames": [ "DealTypeMarket", + "DealTypeF05Paid", "DealTypePDP", "DealTypeDDO" ] @@ -6987,7 +6999,7 @@ } }, "dealType": { - "description": "Deal type: market (f05), pdp (f41), or ddo", + "description": "Deal type: market (legacy f05), f05_paid (f05 with on-chain payments), pdp (f41), or ddo", "type": "string" }, "duration": { @@ -7102,7 +7114,7 @@ } }, "dealType": { - "description": "Deal type: market (f05) or pdp (f41)", + "description": "Deal type: market (legacy f05), f05_paid (f05 with on-chain payments), pdp (f41), or ddo", "type": "string" }, "duration": { diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index ba513bd8..dd0edcc9 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -152,7 +152,8 @@ definitions: deal.ListDealRequest: properties: dealTypes: - description: deal type filter (market for f05, pdp for f41) + description: deal type filter (market/f05_paid for f05, pdp for f41, ddo for + allocations) items: $ref: '#/definitions/model.DealType' type: array @@ -410,6 +411,13 @@ definitions: type: integer errorMessage: type: string + f05PaymentContract: + description: F05 paid-deal fields (only populated for DealTypeF05Paid) + type: string + f05PaymentStatus: + type: string + f05PaymentTxHash: + type: string id: type: integer label: @@ -478,11 +486,13 @@ definitions: model.DealType: enum: - market + - f05_paid - pdp - ddo type: string x-enum-varnames: - DealTypeMarket + - DealTypeF05Paid - DealTypePDP - DealTypeDDO model.File: @@ -754,7 +764,8 @@ definitions: type: string type: array dealType: - description: 'Deal type: market (f05), pdp (f41), or ddo' + description: 'Deal type: market (legacy f05), f05_paid (f05 with on-chain + payments), pdp (f41), or ddo' type: string duration: default: 12840h @@ -843,7 +854,8 @@ definitions: type: string type: array dealType: - description: 'Deal type: market (f05) or pdp (f41)' + description: 'Deal type: market (legacy f05), f05_paid (f05 with on-chain + payments), pdp (f41), or ddo' type: string duration: default: 12840h diff --git a/handler/deal/list.go b/handler/deal/list.go index 2977b79b..93d115f0 100644 --- a/handler/deal/list.go +++ b/handler/deal/list.go @@ -15,7 +15,7 @@ type ListDealRequest struct { Schedules []uint32 `json:"schedules"` // schedule id filter Providers []string `json:"providers"` // provider filter States []model.DealState `json:"states"` // state filter - DealTypes []model.DealType `json:"dealTypes"` // deal type filter (market for f05, pdp for f41) + DealTypes []model.DealType `json:"dealTypes"` // deal type filter (market/f05_paid for f05, pdp for f41, ddo for allocations) } // ListHandler retrieves a list of deals from the database based on the specified filtering criteria in ListDealRequest. diff --git a/handler/deal/schedule/create.go b/handler/deal/schedule/create.go index 53ef7ed7..f9e04bdc 100644 --- a/handler/deal/schedule/create.go +++ b/handler/deal/schedule/create.go @@ -25,7 +25,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), pdp (f41), or ddo + DealType string `json:"dealType"` // Deal type: market (legacy f05), f05_paid (f05 with on-chain payments), pdp (f41), or ddo 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 diff --git a/handler/deal/schedule/create_test.go b/handler/deal/schedule/create_test.go index 97c5f89c..414f6a1a 100644 --- a/handler/deal/schedule/create_test.go +++ b/handler/deal/schedule/create_test.go @@ -246,6 +246,19 @@ func TestCreateHandler_PDPRejectsPreparationWithOversizedPiece(t *testing.T) { }) } +func TestCreateHandler_F05PaidAccepted(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + prep := createPrepWithWallet(t, db, "") + req := createRequest + req.Preparation = fmt.Sprintf("%d", prep.ID) + req.DealType = string(model.DealTypeF05Paid) + + schedule, err := Default.CreateHandler(ctx, db, getMockLotusClient(), req) + require.NoError(t, err) + require.Equal(t, model.DealTypeF05Paid, schedule.DealType) + }) +} + func TestCreateHandler_ProviderNormalizedToActorID(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { createPrepWithWallet(t, db, "") diff --git a/handler/deal/schedule/update.go b/handler/deal/schedule/update.go index fc74c27d..4ca21f23 100644 --- a/handler/deal/schedule/update.go +++ b/handler/deal/schedule/update.go @@ -41,7 +41,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) + DealType *string `json:"dealType"` // Deal type: market (legacy f05), f05_paid (f05 with on-chain payments), pdp (f41), or ddo } // UpdateHandler modifies an existing schedule record based on the provided update request. diff --git a/handler/deal/schedule/update_test.go b/handler/deal/schedule/update_test.go index d91c79d1..7c92167a 100644 --- a/handler/deal/schedule/update_test.go +++ b/handler/deal/schedule/update_test.go @@ -284,6 +284,19 @@ func TestUpdateHandler_DDORequiresURLTemplate(t *testing.T) { }) } +func TestUpdateHandler_F05PaidAccepted(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) + req := UpdateRequest{DealType: ptr.Of(string(model.DealTypeF05Paid))} + schedule, err := Default.UpdateHandler(ctx, db, 1, req) + require.NoError(t, err) + require.Equal(t, model.DealTypeF05Paid, schedule.DealType) + }) +} + func TestUpdateHandler_DDOClearURLTemplateRejected(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Schedule{ diff --git a/model/replication.go b/model/replication.go index 01e38baf..fa5563a8 100644 --- a/model/replication.go +++ b/model/replication.go @@ -10,7 +10,7 @@ type DealState string type ScheduleState string -// DealType represents the type of deal (legacy market vs PDP) +// DealType represents the type of deal (legacy market, paid f05, PDP, etc.) type DealType string const ( @@ -27,6 +27,8 @@ const ( const ( // DealTypeMarket represents legacy f05 market actor deals DealTypeMarket DealType = "market" + // DealTypeF05Paid represents f05 deals with on-chain payments + DealTypeF05Paid DealType = "f05_paid" // DealTypePDP represents f41 PDP (Proof of Data Possession) deals DealTypePDP DealType = "pdp" // DealTypeDDO represents DDO (Decentralized Data Onboarding) allocation deals @@ -68,12 +70,14 @@ var DealStates = []DealState{ var DealTypeStrings = []string{ string(DealTypeMarket), + string(DealTypeF05Paid), string(DealTypePDP), string(DealTypeDDO), } var DealTypes = []DealType{ DealTypeMarket, + DealTypeF05Paid, DealTypePDP, DealTypeDDO, } @@ -134,6 +138,11 @@ type Deal struct { ProofSetLive *bool `json:"proofSetLive,omitempty" table:"verbose"` // ProofSetLive indicates if the proof set is live (actively being challenged) NextChallengeEpoch *int32 `json:"nextChallengeEpoch,omitempty" table:"verbose"` // NextChallengeEpoch is the next epoch when a challenge proof is due + // F05 paid-deal fields (only populated for DealTypeF05Paid) + F05PaymentContract *string `json:"f05PaymentContract,omitempty" table:"verbose"` + F05PaymentTxHash *string `json:"f05PaymentTxHash,omitempty" table:"verbose"` + F05PaymentStatus *string `json:"f05PaymentStatus,omitempty" table:"verbose"` + // DDO-specific fields (only populated for DealTypeDDO) DDOAllocationID *uint64 `json:"ddoAllocationId,omitempty" table:"verbose"` DDORailID *uint64 `json:"ddoRailId,omitempty" table:"verbose"` diff --git a/service/dealpusher/dealpusher.go b/service/dealpusher/dealpusher.go index 6aba9d0c..c81c1714 100644 --- a/service/dealpusher/dealpusher.go +++ b/service/dealpusher/dealpusher.go @@ -45,6 +45,7 @@ type DealPusher struct { lotusClient jsonrpc.RPCClient // Lotus JSON-RPC client for chain queries dealMaker replication.DealMaker // Object responsible for making a deal in replication. pdpProofSetManager PDPProofSetManager // Optional PDP proof set lifecycle manager. + f05PaidDealManager F05PaidDealManager // Optional paid f05 deal lifecycle manager. pdpTxConfirmer PDPTransactionConfirmer // Optional PDP transaction confirmer. pdpSchedulingConfig PDPSchedulingConfig // PDP scheduling config for root batching and tx confirmation. ddoDealManager DDODealManager // Optional DDO deal lifecycle manager. @@ -256,6 +257,11 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) switch d.resolveScheduleDealType(schedule) { case model.DealTypePDP: return d.runPDPSchedule(ctx, schedule) + case model.DealTypeF05Paid: + if d.f05PaidDealManager == nil { + return model.ScheduleError, errors.New("f05 paid scheduling dependencies are not configured") + } + return d.f05PaidDealManager.RunSchedule(ctx, schedule) case model.DealTypeDDO: return d.runDDOSchedule(ctx, schedule) case model.DealTypeMarket: diff --git a/service/dealpusher/f05paid_api.go b/service/dealpusher/f05paid_api.go new file mode 100644 index 00000000..65c97250 --- /dev/null +++ b/service/dealpusher/f05paid_api.go @@ -0,0 +1,14 @@ +package dealpusher + +import ( + "context" + + "github.com/data-preservation-programs/singularity/model" +) + +// F05PaidDealManager owns the paid f05 schedule execution path. +// The first scaffold PR wires the type into Singularity; a later PR will +// provide the concrete implementation backed by the Singularity payments contract. +type F05PaidDealManager interface { + RunSchedule(ctx context.Context, schedule *model.Schedule) (model.ScheduleState, error) +} diff --git a/service/dealpusher/options.go b/service/dealpusher/options.go index 649dd2f6..a798dbd2 100644 --- a/service/dealpusher/options.go +++ b/service/dealpusher/options.go @@ -11,6 +11,12 @@ func WithPDPProofSetManager(manager PDPProofSetManager) Option { } } +func WithF05PaidDealManager(manager F05PaidDealManager) Option { + return func(d *DealPusher) { + d.f05PaidDealManager = manager + } +} + func WithPDPTransactionConfirmer(confirmer PDPTransactionConfirmer) Option { return func(d *DealPusher) { d.pdpTxConfirmer = confirmer diff --git a/service/dealpusher/pdp_wiring_test.go b/service/dealpusher/pdp_wiring_test.go index 8a20423d..e236e3f2 100644 --- a/service/dealpusher/pdp_wiring_test.go +++ b/service/dealpusher/pdp_wiring_test.go @@ -49,6 +49,17 @@ type txConfirmerMock struct { txHash string } +type f05PaidDealManagerMock struct { + schedule *model.Schedule + state model.ScheduleState + err error +} + +func (m *f05PaidDealManagerMock) RunSchedule(_ context.Context, schedule *model.Schedule) (model.ScheduleState, error) { + m.schedule = schedule + return m.state, m.err +} + func (m *txConfirmerMock) WaitForConfirmations(_ context.Context, txHash string, _ uint64, _ time.Duration) (*PDPTransactionReceipt, error) { m.txHash = txHash return &PDPTransactionReceipt{Hash: txHash}, nil @@ -85,6 +96,30 @@ func TestDealPusher_RunSchedule_PDPWithoutDependenciesReturnsConfiguredError(t * require.Contains(t, err.Error(), "pdp scheduling dependencies are not configured") } +func TestDealPusher_RunSchedule_F05PaidWithoutDependenciesReturnsConfiguredError(t *testing.T) { + d := &DealPusher{ + scheduleDealTypeResolver: func(_ *model.Schedule) model.DealType { + return model.DealTypeF05Paid + }, + } + + state, err := d.runSchedule(context.Background(), &model.Schedule{}) + require.Error(t, err) + require.Equal(t, model.ScheduleError, state) + require.Contains(t, err.Error(), "f05 paid scheduling dependencies are not configured") +} + +func TestDealPusher_RunSchedule_F05PaidUsesManager(t *testing.T) { + manager := &f05PaidDealManagerMock{state: model.ScheduleCompleted} + schedule := &model.Schedule{ID: 7, DealType: model.DealTypeF05Paid} + d := &DealPusher{f05PaidDealManager: manager} + + state, err := d.runSchedule(context.Background(), schedule) + require.NoError(t, err) + require.Equal(t, model.ScheduleCompleted, state) + require.Same(t, schedule, manager.schedule) +} + func TestDealPusher_RunSchedule_PDPWithDependenciesCreatesDealsAfterConfirmation(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { clientSubaddr := make([]byte, 20)