diff --git a/api/api.go b/api/api.go index 9371d3d61..8c2d8a2c0 100644 --- a/api/api.go +++ b/api/api.go @@ -31,6 +31,7 @@ import ( "github.com/data-preservation-programs/singularity/service" "github.com/data-preservation-programs/singularity/service/contentprovider" "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/filecoin-project/lassie/pkg/lassie" logging "github.com/ipfs/go-log/v2" "github.com/labstack/echo/v4" @@ -47,6 +48,7 @@ type Server struct { listener net.Listener lotusClient jsonrpc.RPCClient dealMaker replication.DealMaker + keyStore keystore.KeyStore closer io.Closer host host.Host retriever *retriever.Retriever @@ -135,6 +137,10 @@ func InitServer(ctx context.Context, params APIParams) (*Server, error) { endpointfinder.WithErrorLruSize(128), endpointfinder.WithErrorLruTimeout(time.Minute*5), ) + ks, err := keystore.NewLocalKeyStore(wallet.GetKeystoreDir()) + if err != nil { + return nil, errors.Wrap(err, "failed to init keystore") + } return &Server{ db: db, host: h, @@ -146,6 +152,7 @@ func InitServer(ctx context.Context, params APIParams) (*Server, error) { time.Hour, time.Minute*5, ), + keyStore: ks, retriever: retriever.NewRetriever(lassie, endpointFinder), closer: closer, adminHandler: &admin.DefaultHandler{}, @@ -219,6 +226,10 @@ func (s *Server) toEchoHandler(handlerFunc any) echo.HandlerFunc { inputParams = append(inputParams, reflect.ValueOf(s.dealMaker)) continue } + if paramType.String() == "keystore.KeyStore" { + inputParams = append(inputParams, reflect.ValueOf(s.keyStore)) + continue + } if paramType.Kind() == reflect.String || isIntKind(paramType.Kind()) || isUIntKind(paramType.Kind()) { if j >= len(c.ParamValues()) { logger.Error("Invalid handler function signature.") @@ -347,7 +358,7 @@ func (s *Server) setupRoutes(e *echo.Echo) { e.DELETE("/api/preparation/:id/piece/:piece_cid", s.toEchoHandler(s.dataprepHandler.DeletePieceHandler)) // Wallet - e.POST("/api/wallet", s.toEchoHandler(s.walletHandler.ImportHandler)) + e.POST("/api/wallet", s.toEchoHandler(s.walletHandler.ImportKeystoreHandler)) e.GET("/api/wallet", s.toEchoHandler(s.walletHandler.ListHandler)) e.DELETE("/api/wallet/:address", s.toEchoHandler(s.walletHandler.RemoveHandler)) diff --git a/api/api_test.go b/api/api_test.go index ac63411b3..a7fd2c87d 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -35,6 +35,7 @@ import ( "github.com/data-preservation-programs/singularity/replication" "github.com/data-preservation-programs/singularity/service" "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/data-preservation-programs/singularity/util/testutil" "github.com/gotidy/ptr" "github.com/ipfs/go-log/v2" @@ -50,8 +51,8 @@ type MockDealMaker struct { mock.Mock } -func (m *MockDealMaker) MakeDeal(ctx context.Context, walletObj model.Wallet, car model.Car, dealConfig replication.DealConfig) (*model.Deal, error) { - args := m.Called(ctx, walletObj, car, dealConfig) +func (m *MockDealMaker) MakeDeal(ctx context.Context, actorObj model.Actor, car model.Car, dealConfig replication.DealConfig, signer replication.ProposalSigner) (*model.Deal, error) { + args := m.Called(ctx, actorObj, car, dealConfig) return args.Get(0).(*model.Deal), args.Error(1) } @@ -97,7 +98,7 @@ func setupMockDeal() deal.Handler { m := new(deal.MockDeal) m.On("ListHandler", mock.Anything, mock.Anything, mock.Anything). Return([]model.Deal{{}}, nil) - m.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + m.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(&model.Deal{}, nil) return m } @@ -186,13 +187,13 @@ func setupMockWallet() wallet.Handler { Return(&model.Preparation{}, nil) m.On("DetachHandler", mock.Anything, mock.Anything, "id", "wallet"). Return(&model.Preparation{}, nil) - m.On("ImportHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + m.On("ImportKeystoreHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(&model.Wallet{}, nil) m.On("ListHandler", mock.Anything, mock.Anything). Return([]model.Wallet{{}}, nil) m.On("ListAttachedHandler", mock.Anything, mock.Anything, "id"). Return([]model.Wallet{{}}, nil) - m.On("RemoveHandler", mock.Anything, mock.Anything, "wallet"). + m.On("RemoveHandler", mock.Anything, mock.Anything, mock.Anything, "wallet"). Return(nil) return m } @@ -215,11 +216,14 @@ func TestAllAPIs(t *testing.T) { require.NoError(t, err) testutil.One(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + ks, err := keystore.NewLocalKeyStore(t.TempDir()) + require.NoError(t, err) s := Server{ db: db, listener: listener, lotusClient: util.NewLotusClient("", ""), dealMaker: mockDealMaker, + keyStore: ks, closer: io.NopCloser(nil), host: h, adminHandler: mockAdmin, @@ -303,7 +307,7 @@ func TestAllAPIs(t *testing.T) { t.Run("wallet", func(t *testing.T) { t.Run("ImportWallet", func(t *testing.T) { resp, err := client.Wallet.ImportWallet(&wallet2.ImportWalletParams{ - Request: &models.WalletImportRequest{}, + Request: &models.WalletImportKeystoreRequest{}, Context: ctx, }) require.NoError(t, err) diff --git a/client/swagger/http/wallet/import_wallet_parameters.go b/client/swagger/http/wallet/import_wallet_parameters.go index c33f7623d..604dcf31e 100644 --- a/client/swagger/http/wallet/import_wallet_parameters.go +++ b/client/swagger/http/wallet/import_wallet_parameters.go @@ -67,7 +67,7 @@ type ImportWalletParams struct { Request body */ - Request *models.WalletImportRequest + Request *models.WalletImportKeystoreRequest timeout time.Duration Context context.Context @@ -123,13 +123,13 @@ func (o *ImportWalletParams) SetHTTPClient(client *http.Client) { } // WithRequest adds the request to the import wallet params -func (o *ImportWalletParams) WithRequest(request *models.WalletImportRequest) *ImportWalletParams { +func (o *ImportWalletParams) WithRequest(request *models.WalletImportKeystoreRequest) *ImportWalletParams { o.SetRequest(request) return o } // SetRequest adds the request to the import wallet params -func (o *ImportWalletParams) SetRequest(request *models.WalletImportRequest) { +func (o *ImportWalletParams) SetRequest(request *models.WalletImportKeystoreRequest) { o.Request = request } diff --git a/client/swagger/models/model_wallet.go b/client/swagger/models/model_wallet.go index dc2ff9fe3..335f3129d 100644 --- a/client/swagger/models/model_wallet.go +++ b/client/swagger/models/model_wallet.go @@ -17,14 +17,23 @@ import ( // swagger:model model.Wallet type ModelWallet struct { - // Address is the Filecoin full address of the wallet + // nullable, links to on-chain actor f0... + ActorID string `json:"actorId,omitempty"` + + // filecoin address (f1.../f3...) Address string `json:"address,omitempty"` - // ID is the short ID of the wallet - ID string `json:"id,omitempty"` + // id + ID int64 `json:"id,omitempty"` + + // absolute path to key file + KeyPath string `json:"keyPath,omitempty"` + + // local, yubikey, aws-kms, etc + KeyStore string `json:"keyStore,omitempty"` - // PrivateKey is the private key of the wallet - PrivateKey string `json:"privateKey,omitempty"` + // optional label + Name string `json:"name,omitempty"` } // Validate validates this model wallet diff --git a/client/swagger/models/wallet_import_keystore_request.go b/client/swagger/models/wallet_import_keystore_request.go new file mode 100644 index 000000000..086fc6002 --- /dev/null +++ b/client/swagger/models/wallet_import_keystore_request.go @@ -0,0 +1,53 @@ +// 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" +) + +// WalletImportKeystoreRequest wallet import keystore request +// +// swagger:model wallet.ImportKeystoreRequest +type WalletImportKeystoreRequest struct { + + // optional human-readable name + Name string `json:"name,omitempty"` + + // lotus wallet export format + PrivateKey string `json:"privateKey,omitempty"` +} + +// Validate validates this wallet import keystore request +func (m *WalletImportKeystoreRequest) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this wallet import keystore request based on context it is used +func (m *WalletImportKeystoreRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *WalletImportKeystoreRequest) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *WalletImportKeystoreRequest) UnmarshalBinary(b []byte) error { + var res WalletImportKeystoreRequest + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/client/swagger/models/wallet_import_request.go b/client/swagger/models/wallet_import_request.go deleted file mode 100644 index 66d55e15b..000000000 --- a/client/swagger/models/wallet_import_request.go +++ /dev/null @@ -1,50 +0,0 @@ -// Code generated by go-swagger; DO NOT EDIT. - -package models - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -import ( - "context" - - "github.com/go-openapi/strfmt" - "github.com/go-openapi/swag" -) - -// WalletImportRequest wallet import request -// -// swagger:model wallet.ImportRequest -type WalletImportRequest struct { - - // This is the exported private key from lotus wallet export - PrivateKey string `json:"privateKey,omitempty"` -} - -// Validate validates this wallet import request -func (m *WalletImportRequest) Validate(formats strfmt.Registry) error { - return nil -} - -// ContextValidate validates this wallet import request based on context it is used -func (m *WalletImportRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error { - return nil -} - -// MarshalBinary interface implementation -func (m *WalletImportRequest) MarshalBinary() ([]byte, error) { - if m == nil { - return nil, nil - } - return swag.WriteJSON(m) -} - -// UnmarshalBinary interface implementation -func (m *WalletImportRequest) UnmarshalBinary(b []byte) error { - var res WalletImportRequest - if err := swag.ReadJSON(b, &res); err != nil { - return err - } - *m = res - return nil -} diff --git a/cmd/dataprep_test.go b/cmd/dataprep_test.go index 85db643a5..57012f9d4 100644 --- a/cmd/dataprep_test.go +++ b/cmd/dataprep_test.go @@ -24,9 +24,7 @@ var testPreparation = model.Preparation{ MaxSize: 100, PieceSize: 200, Wallets: []model.Wallet{{ - ID: "client_id", - Address: "client_address", - PrivateKey: "private_key", + Address: "client_address", KeyPath: "/tmp/key", KeyStore: "local", }}, SourceStorages: []model.Storage{{ ID: 1, diff --git a/cmd/deal/send-manual.go b/cmd/deal/send-manual.go index f01538d79..46bb1a171 100644 --- a/cmd/deal/send-manual.go +++ b/cmd/deal/send-manual.go @@ -8,9 +8,11 @@ import ( "github.com/data-preservation-programs/singularity/cmd/cliutil" "github.com/data-preservation-programs/singularity/database" "github.com/data-preservation-programs/singularity/handler/deal" + "github.com/data-preservation-programs/singularity/handler/wallet" "github.com/data-preservation-programs/singularity/replication" "github.com/data-preservation-programs/singularity/service/epochutil" "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/urfave/cli/v2" ) @@ -176,13 +178,15 @@ Notes: return errors.Wrap(err, "failed to init host") } defer h.Close() - dealMaker := replication.NewDealMaker( - util.NewLotusClient(c.String("lotus-api"), c.String("lotus-token")), - h, - 10*timeout, - timeout, - ) - dealModel, err := deal.Default.SendManualHandler(ctx, db, dealMaker, proposal) + + ks, err := keystore.NewLocalKeyStore(wallet.GetKeystoreDir()) + if err != nil { + return errors.Wrap(err, "failed to init keystore") + } + + lotusClient := util.NewLotusClient(c.String("lotus-api"), c.String("lotus-token")) + dealMaker := replication.NewDealMaker(lotusClient, h, 10*timeout, timeout) + dealModel, err := deal.Default.SendManualHandler(ctx, db, ks, lotusClient, dealMaker, proposal) if err != nil { return errors.WithStack(err) } diff --git a/cmd/deal_test.go b/cmd/deal_test.go index 4430543ba..35c936546 100644 --- a/cmd/deal_test.go +++ b/cmd/deal_test.go @@ -24,13 +24,13 @@ func swapDealHandler(mockHandler deal.Handler) func() { func TestSendDealHandler(t *testing.T) { testutil.One(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&model.Wallet{ID: "client_id"}).Error + err := db.Create(&model.Actor{ID: "client_id"}).Error require.NoError(t, err) runner := NewRunner() defer runner.Save(t) mockHandler := new(deal.MockDeal) defer swapDealHandler(mockHandler)() - mockHandler.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ + mockHandler.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ State: "proposed", Provider: "f01", ProposalID: "proposal_id", @@ -46,7 +46,7 @@ func TestSendDealHandler(t *testing.T) { }, nil).Once() _, _, err = runner.Run(ctx, "singularity deal send-manual --client client --provider provider --piece-cid piece_cid --piece-size 1024 --save") require.NoError(t, err) - mockHandler.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ + mockHandler.On("SendManualHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ State: "proposed", Provider: "f01", ProposalID: "proposal_id", diff --git a/cmd/wallet/import.go b/cmd/wallet/import.go index 2e3547afe..7d7458582 100644 --- a/cmd/wallet/import.go +++ b/cmd/wallet/import.go @@ -9,13 +9,13 @@ import ( "github.com/data-preservation-programs/singularity/cmd/cliutil" "github.com/data-preservation-programs/singularity/database" "github.com/data-preservation-programs/singularity/handler/wallet" - "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/urfave/cli/v2" ) var ImportCmd = &cli.Command{ Name: "import", - Usage: "Import a wallet from exported private key", + Usage: "Import a wallet from a private key file into the keystore", ArgsUsage: "[path, or stdin if omitted]", Action: func(c *cli.Context) error { db, closer, err := database.OpenFromCLI(c) @@ -41,12 +41,16 @@ var ImportCmd = &cli.Command{ } } - lotusClient := util.NewLotusClient(c.String("lotus-api"), c.String("lotus-token")) - w, err := wallet.Default.ImportHandler( + ks, err := keystore.NewLocalKeyStore(wallet.GetKeystoreDir()) + if err != nil { + return errors.Wrap(err, "failed to init keystore") + } + + w, err := wallet.Default.ImportKeystoreHandler( c.Context, db, - lotusClient, - wallet.ImportRequest{ + ks, + wallet.ImportKeystoreRequest{ PrivateKey: privateKey, }) if err != nil { diff --git a/cmd/wallet/remove.go b/cmd/wallet/remove.go index 859e2dcae..0d112547d 100644 --- a/cmd/wallet/remove.go +++ b/cmd/wallet/remove.go @@ -5,6 +5,7 @@ import ( "github.com/data-preservation-programs/singularity/cmd/cliutil" "github.com/data-preservation-programs/singularity/database" "github.com/data-preservation-programs/singularity/handler/wallet" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/urfave/cli/v2" ) @@ -25,6 +26,10 @@ var RemoveCmd = &cli.Command{ return errors.WithStack(err) } defer closer.Close() - return wallet.Default.RemoveHandler(c.Context, db, c.Args().Get(0)) + ks, err := keystore.NewLocalKeyStore(wallet.GetKeystoreDir()) + if err != nil { + return errors.WithStack(err) + } + return wallet.Default.RemoveHandler(c.Context, db, ks, c.Args().Get(0)) }, } diff --git a/cmd/wallet_test.go b/cmd/wallet_test.go index ff2892cd4..96762c03b 100644 --- a/cmd/wallet_test.go +++ b/cmd/wallet_test.go @@ -32,10 +32,9 @@ func TestWalletImport(t *testing.T) { defer runner.Save(t) mockHandler := new(wallet.MockWallet) defer swapWalletHandler(mockHandler)() - mockHandler.On("ImportHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Wallet{ - ID: "id", - Address: "address", - PrivateKey: "private", + mockHandler.On("ImportKeystoreHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Wallet{ + ID: 1, + Address: "address", }, nil) _, _, err = runner.Run(ctx, "singularity wallet import "+testutil.EscapePath(filepath.Join(tmp, "private"))) require.NoError(t, err) @@ -51,13 +50,11 @@ func TestWalletList(t *testing.T) { mockHandler := new(wallet.MockWallet) defer swapWalletHandler(mockHandler)() mockHandler.On("ListHandler", mock.Anything, mock.Anything).Return([]model.Wallet{{ - ID: "id1", - Address: "address1", - PrivateKey: "private1", + ID: 1, + Address: "address1", }, { - ID: "id2", - Address: "address2", - PrivateKey: "private2", + ID: 2, + Address: "address2", }}, nil) _, _, err := runner.Run(ctx, "singularity wallet list") require.NoError(t, err) @@ -72,7 +69,7 @@ func TestWalletRemove(t *testing.T) { defer runner.Save(t) mockHandler := new(wallet.MockWallet) defer swapWalletHandler(mockHandler)() - mockHandler.On("RemoveHandler", mock.Anything, mock.Anything, mock.Anything).Return(nil) + mockHandler.On("RemoveHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) _, _, err := runner.Run(ctx, "singularity wallet remove --really-do-it xxx") require.NoError(t, err) }) @@ -84,7 +81,7 @@ func TestWalletRemove_NoReallyDoIt(t *testing.T) { defer runner.Save(t) mockHandler := new(wallet.MockWallet) defer swapWalletHandler(mockHandler)() - mockHandler.On("RemoveHandler", mock.Anything, mock.Anything, mock.Anything).Return(nil) + mockHandler.On("RemoveHandler", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) _, _, err := runner.Run(ctx, "singularity wallet remove xxx") require.ErrorIs(t, err, cliutil.ErrReallyDoIt) }) diff --git a/docs/en/cli-reference/wallet/README.md b/docs/en/cli-reference/wallet/README.md index 7096416ac..0e4e3b182 100644 --- a/docs/en/cli-reference/wallet/README.md +++ b/docs/en/cli-reference/wallet/README.md @@ -9,7 +9,7 @@ USAGE: singularity wallet command [command options] COMMANDS: - import Import a wallet from exported private key + import Import a wallet from a private key file into the keystore list List all imported wallets remove Remove a wallet help, h Shows a list of commands or help for one command diff --git a/docs/en/cli-reference/wallet/import.md b/docs/en/cli-reference/wallet/import.md index 9685a6048..6d5d99f5b 100644 --- a/docs/en/cli-reference/wallet/import.md +++ b/docs/en/cli-reference/wallet/import.md @@ -1,9 +1,9 @@ -# Import a wallet from exported private key +# Import a wallet from a private key file into the keystore {% code fullWidth="true" %} ``` NAME: - singularity wallet import - Import a wallet from exported private key + singularity wallet import - Import a wallet from a private key file into the keystore USAGE: singularity wallet import [command options] [path, or stdin if omitted] diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 364b2c6fc..52e9c2211 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -5892,7 +5892,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/wallet.ImportRequest" + "$ref": "#/definitions/wallet.ImportKeystoreRequest" } } ], @@ -6936,16 +6936,27 @@ const docTemplate = `{ "model.Wallet": { "type": "object", "properties": { + "actorId": { + "description": "nullable, links to on-chain actor f0...", + "type": "string" + }, "address": { - "description": "Address is the Filecoin full address of the wallet", + "description": "filecoin address (f1.../f3...)", "type": "string" }, "id": { - "description": "ID is the short ID of the wallet", + "type": "integer" + }, + "keyPath": { + "description": "absolute path to key file", "type": "string" }, - "privateKey": { - "description": "PrivateKey is the private key of the wallet", + "keyStore": { + "description": "local, yubikey, aws-kms, etc", + "type": "string" + }, + "name": { + "description": "optional label", "type": "string" } } @@ -20328,11 +20339,15 @@ const docTemplate = `{ "store.PieceReader": { "type": "object" }, - "wallet.ImportRequest": { + "wallet.ImportKeystoreRequest": { "type": "object", "properties": { + "name": { + "description": "optional human-readable name", + "type": "string" + }, "privateKey": { - "description": "This is the exported private key from lotus wallet export", + "description": "lotus wallet export format", "type": "string" } } diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 2c27e1b61..d8689f270 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -5885,7 +5885,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/wallet.ImportRequest" + "$ref": "#/definitions/wallet.ImportKeystoreRequest" } } ], @@ -6929,16 +6929,27 @@ "model.Wallet": { "type": "object", "properties": { + "actorId": { + "description": "nullable, links to on-chain actor f0...", + "type": "string" + }, "address": { - "description": "Address is the Filecoin full address of the wallet", + "description": "filecoin address (f1.../f3...)", "type": "string" }, "id": { - "description": "ID is the short ID of the wallet", + "type": "integer" + }, + "keyPath": { + "description": "absolute path to key file", "type": "string" }, - "privateKey": { - "description": "PrivateKey is the private key of the wallet", + "keyStore": { + "description": "local, yubikey, aws-kms, etc", + "type": "string" + }, + "name": { + "description": "optional label", "type": "string" } } @@ -20321,11 +20332,15 @@ "store.PieceReader": { "type": "object" }, - "wallet.ImportRequest": { + "wallet.ImportKeystoreRequest": { "type": "object", "properties": { + "name": { + "description": "optional human-readable name", + "type": "string" + }, "privateKey": { - "description": "This is the exported private key from lotus wallet export", + "description": "lotus wallet export format", "type": "string" } } diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index d6c215da9..d9d366c9b 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -712,14 +712,22 @@ definitions: type: object model.Wallet: properties: + actorId: + description: nullable, links to on-chain actor f0... + type: string address: - description: Address is the Filecoin full address of the wallet + description: filecoin address (f1.../f3...) type: string id: - description: ID is the short ID of the wallet + type: integer + keyPath: + description: absolute path to key file type: string - privateKey: - description: PrivateKey is the private key of the wallet + keyStore: + description: local, yubikey, aws-kms, etc + type: string + name: + description: optional label type: string type: object schedule.CreateRequest: @@ -11103,10 +11111,13 @@ definitions: type: object store.PieceReader: type: object - wallet.ImportRequest: + wallet.ImportKeystoreRequest: properties: + name: + description: optional human-readable name + type: string privateKey: - description: This is the exported private key from lotus wallet export + description: lotus wallet export format type: string type: object externalDocs: @@ -14987,7 +14998,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/wallet.ImportRequest' + $ref: '#/definitions/wallet.ImportKeystoreRequest' produces: - application/json responses: diff --git a/go.mod b/go.mod index 63dfea190..8eedd7cb4 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/bcicen/jstream v1.0.1 github.com/brianvoe/gofakeit/v6 v6.23.2 github.com/cockroachdb/errors v1.11.3 - github.com/data-preservation-programs/go-synapse v0.0.0-20260206105716-b6a5e7e6808e + github.com/data-preservation-programs/go-synapse v0.0.0-20260220134035-b27d67ac9095 github.com/data-preservation-programs/table v0.0.3 github.com/dustin/go-humanize v1.0.1 github.com/ethereum/go-ethereum v1.14.12 @@ -52,7 +52,6 @@ require ( github.com/jackc/pgx/v5 v5.7.6 github.com/jellydator/ttlcache/v3 v3.0.1 github.com/joho/godotenv v1.5.1 - github.com/jsign/go-filsigner v0.4.1 github.com/klauspost/compress v1.18.1 github.com/labstack/echo/v4 v4.10.2 github.com/libp2p/go-libp2p v0.44.0 @@ -152,15 +151,12 @@ require ( github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect - github.com/dchest/blake2b v1.0.0 // indirect github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v28.5.1+incompatible // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/drand/kyber v1.3.1 // indirect - github.com/drand/kyber-bls12381 v0.3.3 // indirect github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5 // indirect github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 // indirect github.com/emersion/go-message v0.18.2 // indirect @@ -259,7 +255,6 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/jtolio/noiseconn v0.0.0-20231127013910-f6d9ecbf1de7 // indirect github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 // indirect - github.com/kilic/bls12-381 v0.1.1-0.20220929213557-ca162e8a70f4 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/koofr/go-httpclient v0.0.0-20240520111329-e20f8f203988 // indirect github.com/koofr/go-koofrclient v0.0.0-20221207135200-cbd7fc9ad6a6 // indirect @@ -285,6 +280,7 @@ require ( github.com/miekg/dns v1.1.68 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect + github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -359,7 +355,7 @@ require ( github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/supranational/blst v0.3.13 // indirect + github.com/supranational/blst v0.3.16 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/t3rm1n4l/go-mega v0.0.0-20250926104142-ccb8d3498e6c // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect diff --git a/go.sum b/go.sum index 045fe5401..7bd85a348 100644 --- a/go.sum +++ b/go.sum @@ -251,8 +251,8 @@ github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+ github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA= github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= -github.com/data-preservation-programs/go-synapse v0.0.0-20260206105716-b6a5e7e6808e h1:xqTd8FN2XAxKVwbkNmkSeemTFkuGp6eDMTvTDhumCxQ= -github.com/data-preservation-programs/go-synapse v0.0.0-20260206105716-b6a5e7e6808e/go.mod h1:5pXdfN2ywCsZK5gbhtmR0Nv6ttEAeUgHIq1gW3QCMPg= +github.com/data-preservation-programs/go-synapse v0.0.0-20260220134035-b27d67ac9095 h1:pFMqjtl0phxSg6L3EKkD6OGDwUha7ReterF09v2f184= +github.com/data-preservation-programs/go-synapse v0.0.0-20260220134035-b27d67ac9095/go.mod h1:qgzPsiGWjTPT/oACA6Uj1+WsASwsYFW/iJ8AWacJdjc= github.com/data-preservation-programs/table v0.0.3 h1:hboeauxPXybE8KlMA+RjDXz/J4xaG5CAFCcxyOm8yWo= github.com/data-preservation-programs/table v0.0.3/go.mod h1:sRGP/IuuqFc/y9QfmDyb5h6Q2wrnhhnBofEOj9aDRJg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -261,15 +261,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= -github.com/dchest/blake2b v1.0.0 h1:KK9LimVmE0MjRl9095XJmKqZ+iLxWATvlcpVFRtaw6s= -github.com/dchest/blake2b v1.0.0/go.mod h1:U034kXgbJpCle2wSk5ybGIVhOSHCVLMDqOzcPEA0F7s= github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= -github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210507181900-4e0be8d2fbb4/go.mod h1:UkVqoxmJlLgUvBjJD+GdJz6mgdSdf3UjX83xfwUAYDk= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -280,16 +275,6 @@ github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pM github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/drand/bls12-381 v0.3.2/go.mod h1:dtcLgPtYT38L3NO6mPDYH0nbpc5tjPassDqiniuAt4Y= -github.com/drand/kyber v1.0.1-0.20200110225416-8de27ed8c0e2/go.mod h1:UpXoA0Upd1N9l4TvRPHr1qAUBBERj6JQ/mnKI3BPEmw= -github.com/drand/kyber v1.0.2/go.mod h1:x6KOpK7avKj0GJ4emhXFP5n7M7W7ChAPmnQh/OL6vRw= -github.com/drand/kyber v1.1.4/go.mod h1:9+IgTq7kadePhZg7eRwSD7+bA+bmvqRK+8DtmoV5a3U= -github.com/drand/kyber v1.3.1 h1:E0p6M3II+loMVwTlAp5zu4+GGZFNiRfq02qZxzw2T+Y= -github.com/drand/kyber v1.3.1/go.mod h1:f+mNHjiGT++CuueBrpeMhFNdKZAsy0tu03bKq9D5LPA= -github.com/drand/kyber-bls12381 v0.2.0/go.mod h1:zQip/bHdeEB6HFZSU3v+d3cQE0GaBVQw9aR2E7AdoeI= -github.com/drand/kyber-bls12381 v0.2.1/go.mod h1:JwWn4nHO9Mp4F5qCie5sVIPQZ0X6cw8XAeMRvc/GXBE= -github.com/drand/kyber-bls12381 v0.3.3 h1:sLl0ILJtB4+POHAKq6tdnWyg+iXADE0LjVKN91RI8JI= -github.com/drand/kyber-bls12381 v0.3.3/go.mod h1:uVRWtcZDAApOWFMwoJVcTfC4csVxXmpkdoSCUZJ5QOY= github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5 h1:FT+t0UEDykcor4y3dMVKXIiWJETBpRgERYTGlmMd7HU= github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5/go.mod h1:rSS3kM9XMzSQ6pw91Qgd6yB5jdt70N4OdtrAf74As5M= github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI= @@ -320,12 +305,9 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/filecoin-project/dagstore v0.5.2 h1:Nd6oXdnolbbVhpMpkYT5PJHOjQp4OBSntHpMV5pxj3c= github.com/filecoin-project/dagstore v0.5.2/go.mod h1:mdqKzYrRBHf1pRMthYfMv3n37oOw0Tkx7+TxPt240M0= -github.com/filecoin-project/filecoin-ffi v0.30.4-0.20200910194244-f640612a1a1f/go.mod h1:+If3s2VxyjZn+KGGZIoRXBDSFQ9xL404JBJGf4WhEj0= github.com/filecoin-project/filecoin-ffi v1.34.0 h1:OvcsvsFUCwzLOGT949dsJEqSLyGx4d8TPPRrmrzlQbk= github.com/filecoin-project/filecoin-ffi v1.34.0/go.mod h1:AXLJk1PscWAwEa9CdqdiFwj1ttVJ+UIm8YQDPpTqBjg= -github.com/filecoin-project/go-address v0.0.3/go.mod h1:jr8JxKsYx+lQlQZmF5i2U0Z+cGQ59wMIps/8YW/lDj8= github.com/filecoin-project/go-address v0.0.5/go.mod h1:jr8JxKsYx+lQlQZmF5i2U0Z+cGQ59wMIps/8YW/lDj8= -github.com/filecoin-project/go-address v1.1.0/go.mod h1:5t3z6qPmIADZBtuE9EIzi0EwzcRy2nVhpo0I/c1r0OA= github.com/filecoin-project/go-address v1.2.0 h1:NHmWUE/J7Pi2JZX3gZt32XuY69o9StVZeJxdBodIwOE= github.com/filecoin-project/go-address v1.2.0/go.mod h1:kQEQ4qZ99a51X7DjT9HiMT4yR6UwLJ9kznlxsOIeDAg= github.com/filecoin-project/go-amt-ipld/v2 v2.1.0 h1:t6qDiuGYYngDqaLc2ZUvdtAg4UNxPeOYaXhBWSNsVaM= @@ -333,7 +315,6 @@ github.com/filecoin-project/go-amt-ipld/v2 v2.1.0/go.mod h1:nfFPoGyX0CU9SkXX8EoC github.com/filecoin-project/go-amt-ipld/v4 v4.0.0/go.mod h1:gF053YQ4BIpzTNDoEwHZas7U3oAwncDVGvOHyY8oDpE= github.com/filecoin-project/go-amt-ipld/v4 v4.4.0 h1:6kvvMeSpIy4GTU5t3vPHZgWYIMRzGRKLJ73s/cltsoc= github.com/filecoin-project/go-amt-ipld/v4 v4.4.0/go.mod h1:msgmUxTyRBZ6iXt+5dnUDnIb7SEFqdPsbB1wyo/G3ts= -github.com/filecoin-project/go-bitfield v0.2.0/go.mod h1:CNl9WG8hgR5mttCnUErjcQjGvuiZjRqK9rHVBsQF4oM= github.com/filecoin-project/go-bitfield v0.2.4 h1:uZ7MeE+XfM5lqrHJZ93OnhQKc/rveW8p9au0C68JPgk= github.com/filecoin-project/go-bitfield v0.2.4/go.mod h1:CNl9WG8hgR5mttCnUErjcQjGvuiZjRqK9rHVBsQF4oM= github.com/filecoin-project/go-cbor-util v0.0.0-20191219014500-08c40a1e63a2/go.mod h1:pqTiPHobNkOVM5thSRsHYjyQfq7O5QSCMhvuu9JoDlg= @@ -341,10 +322,8 @@ github.com/filecoin-project/go-cbor-util v0.0.2 h1:vljF+a+NBwv89VfPvy5lJEtrZWe8k github.com/filecoin-project/go-cbor-util v0.0.2/go.mod h1:96OIHk38Y1IV+KCXkGjE2WjjIxfpIanz2rWIIy5kKkQ= github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= -github.com/filecoin-project/go-commp-utils v0.1.3/go.mod h1:3ENlD1pZySaUout0p9ANQrY3fDFoXdqyX04J+dWpK30= github.com/filecoin-project/go-commp-utils v0.1.4 h1:/WSsrAb0xupo+aRWRyD80lRUXAXJvYoTgDQS1pYZ1Mk= github.com/filecoin-project/go-commp-utils v0.1.4/go.mod h1:Sekocu5q9b4ECAUFu853GFUbm8I7upAluummHFe2kFo= -github.com/filecoin-project/go-commp-utils/nonffi v0.0.0-20220905160352-62059082a837/go.mod h1:e2YBjSblNVoBckkbv3PPqsq71q98oFkFqL7s1etViGo= github.com/filecoin-project/go-commp-utils/v2 v2.1.0 h1:KWNRalUp2bhN1SW7STsJS2AHs9mnfGKk9LnQgzDe+gI= github.com/filecoin-project/go-commp-utils/v2 v2.1.0/go.mod h1:NbxJYlhxtWaNhlVCj/gysLNu26kYII83IV5iNrAO9iI= github.com/filecoin-project/go-crypto v0.0.0-20191218222705-effae4ea9f03/go.mod h1:+viYnvGtUTgJRdy6oaeF4MTFKAfatX071MPDPBL11EQ= @@ -354,9 +333,6 @@ github.com/filecoin-project/go-data-transfer/v2 v2.0.0-rc8 h1:EWC89lM/tJAjyzaxZ6 github.com/filecoin-project/go-data-transfer/v2 v2.0.0-rc8/go.mod h1:mK3/NbSljx3Kr335+IXEe8gcdEPA2eZXJaNhodK9bAI= github.com/filecoin-project/go-ds-versioning v0.1.2 h1:to4pTadv3IeV1wvgbCbN6Vqd+fu+7tveXgv/rCEZy6w= github.com/filecoin-project/go-ds-versioning v0.1.2/go.mod h1:C9/l9PnB1+mwPa26BBVpCjG/XQCB0yj/q5CK2J8X1I4= -github.com/filecoin-project/go-fil-commcid v0.0.0-20200716160307-8f644712406f/go.mod h1:Eaox7Hvus1JgPrL5+M3+h7aSPHc0cVqpSxA+TxIEpZQ= -github.com/filecoin-project/go-fil-commcid v0.0.0-20201016201715-d41df56b4f6a/go.mod h1:Eaox7Hvus1JgPrL5+M3+h7aSPHc0cVqpSxA+TxIEpZQ= -github.com/filecoin-project/go-fil-commcid v0.1.0/go.mod h1:Eaox7Hvus1JgPrL5+M3+h7aSPHc0cVqpSxA+TxIEpZQ= github.com/filecoin-project/go-fil-commcid v0.2.0 h1:B+5UX8XGgdg/XsdUpST4pEBviKkFOw+Fvl2bLhSKGpI= github.com/filecoin-project/go-fil-commcid v0.2.0/go.mod h1:8yigf3JDIil+/WpqR5zoKyP0jBPCOGtEqq/K1CcMy9Q= github.com/filecoin-project/go-fil-commp-hashhash v0.2.1-0.20230811065821-2e9c683db589 h1:PP5FU5JVVDb7zODWZlgzbdmQDtwu3Mm0bK9Bg/Om5yc= @@ -368,17 +344,12 @@ github.com/filecoin-project/go-hamt-ipld v0.1.5/go.mod h1:6Is+ONR5Cd5R6XZoCse1CW github.com/filecoin-project/go-hamt-ipld/v3 v3.1.0/go.mod h1:bxmzgT8tmeVQA1/gvBwFmYdT8SOFUwB3ovSUfG1Ux0g= github.com/filecoin-project/go-hamt-ipld/v3 v3.4.1 h1:wl+ZHruCcE9LvwU7blpwWn35XOcRS6+IBg75G7ZzxzY= github.com/filecoin-project/go-hamt-ipld/v3 v3.4.1/go.mod h1:AqjryNfkxffpnqsa5mwnJHlazhVqF6W2nilu+VYKIq8= -github.com/filecoin-project/go-padreader v0.0.0-20200903213702-ed5fae088b20/go.mod h1:mPn+LRRd5gEKNAtc+r3ScpW2JRU/pj4NBKdADYWHiak= github.com/filecoin-project/go-padreader v0.0.1 h1:8h2tVy5HpoNbr2gBRr+WD6zV6VD6XHig+ynSGJg8ZOs= github.com/filecoin-project/go-padreader v0.0.1/go.mod h1:VYVPJqwpsfmtoHnAmPx6MUwmrK6HIcDqZJiuZhtmfLQ= github.com/filecoin-project/go-retrieval-types v1.2.0 h1:fz6DauLVP3GRg7UuW7HZ6sE+GTmaUW70DTXBF1r9cK0= github.com/filecoin-project/go-retrieval-types v1.2.0/go.mod h1:ojW6wSw2GPyoRDBGqw1K6JxUcbfa5NOSIiyQEeh7KK0= github.com/filecoin-project/go-state-types v0.0.0-20200903145444-247639ffa6ad/go.mod h1:IQ0MBPnonv35CJHtWSN3YY1Hz2gkPru1Q9qoaYLxx9I= -github.com/filecoin-project/go-state-types v0.0.0-20200904021452-1883f36ca2f4/go.mod h1:IQ0MBPnonv35CJHtWSN3YY1Hz2gkPru1Q9qoaYLxx9I= -github.com/filecoin-project/go-state-types v0.0.0-20201102161440-c8033295a1fc/go.mod h1:ezYnPf0bNkTsDibL/psSz5dy4B5awOJ/E7P2Saeep8g= github.com/filecoin-project/go-state-types v0.1.6/go.mod h1:UwGVoMsULoCK+bWjEdd/xLCvLAQFBC7EDT477SKml+Q= -github.com/filecoin-project/go-state-types v0.1.10/go.mod h1:UwGVoMsULoCK+bWjEdd/xLCvLAQFBC7EDT477SKml+Q= -github.com/filecoin-project/go-state-types v0.10.0/go.mod h1:aLIas+W8BWAfpLWEPUOGMPBdhcVwoCG4pIQSQk26024= github.com/filecoin-project/go-state-types v0.17.0 h1:HpBb6G+VSOOI6rQFSnvPVyRsnms8je94cwvU69DJ+9Y= github.com/filecoin-project/go-state-types v0.17.0/go.mod h1:em4yo9mglrdyHbcsxelHCSKMjLdJLddLERWQe6J8vYc= github.com/filecoin-project/go-statemachine v0.0.0-20200925024713-05bd7c71fbfe/go.mod h1:FGwQgZAt2Gh5mjlwJUlVB62JeYdo+if0xWxSEfBD9ig= @@ -387,7 +358,6 @@ github.com/filecoin-project/go-statemachine v1.0.3/go.mod h1:jZdXXiHa61n4NmgWFG4 github.com/filecoin-project/go-statestore v0.1.0/go.mod h1:LFc9hD+fRxPqiHiaqUEZOinUJB4WARkRfNl10O7kTnI= github.com/filecoin-project/go-statestore v0.2.0 h1:cRRO0aPLrxKQCZ2UOQbzFGn4WDNdofHZoGPjfNaAo5Q= github.com/filecoin-project/go-statestore v0.2.0/go.mod h1:8sjBYbS35HwPzct7iT4lIXjLlYyPor80aU7t7a/Kspo= -github.com/filecoin-project/specs-actors v0.9.4/go.mod h1:BStZQzx5x7TmCkLv0Bpa07U6cPKol6fd3w9KjMPZ6Z4= github.com/filecoin-project/specs-actors v0.9.15 h1:3VpKP5/KaDUHQKAMOg4s35g/syDaEBueKLws0vbsjMc= github.com/filecoin-project/specs-actors v0.9.15/go.mod h1:pjGEe3QlWtK20ju/aFRsiArbMX6Cn8rqEhhsiCM9xYE= github.com/filecoin-shipyard/boostly v0.0.0-20230813165216-a449c35ece79 h1:MdF/QWskzWeBRpnffuJk+qjQWhwJMLSyHpoj679Xsdo= @@ -683,7 +653,6 @@ github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67Fexh github.com/ipfs/go-cid v0.0.6-0.20200501230655-7c82f3b81c00/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67FexhXog= github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= github.com/ipfs/go-cid v0.0.7/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= -github.com/ipfs/go-cid v0.2.0/go.mod h1:P+HXFDF4CVhaVayiEb4wkAy7zBHxBwsJyt0Y5U6MLro= github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= github.com/ipfs/go-cidutil v0.1.0 h1:RW5hO7Vcf16dplUU60Hs0AKDkQAVPVplr7lk97CFL+Q= @@ -699,7 +668,6 @@ github.com/ipfs/go-dsqueue v0.0.5 h1:TUOk15TlCJ/NKV8Yk2W5wgkEjDa44Nem7a7FGIjsMNU github.com/ipfs/go-dsqueue v0.0.5/go.mod h1:i/jAlpZjBbQJLioN+XKbFgnd+u9eAhGZs9IrqIzTd9g= github.com/ipfs/go-graphsync v0.18.0 h1:b+DNJ4lWsCKKaVKYgqgt4rrqshvVcTphN7Rl0JfFiD4= github.com/ipfs/go-graphsync v0.18.0/go.mod h1:+7SU0L6thSFFTo1pbDcwCqi+gN0L7UQP8eVm897Mg0s= -github.com/ipfs/go-hamt-ipld v0.1.1/go.mod h1:1EZCr2v0jlCnhpa+aZ0JZYp8Tt2w16+JJOAVz17YcDk= github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= github.com/ipfs/go-ipfs-blocksutil v0.0.2 h1:hoAV68CQOzUa/e1egCME3lbrsyEGO0pY7Bb26T+8/Zc= @@ -729,9 +697,7 @@ github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCm github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= github.com/ipfs/go-ipld-cbor v0.0.3/go.mod h1:wTBtrQZA3SoFKMVkp6cn6HMRteIB1VsmHA0AQFOn7Nc= github.com/ipfs/go-ipld-cbor v0.0.4/go.mod h1:BkCduEx3XBCO6t2Sfo5BaHzuok7hbhdMm9Oh8B2Ftq4= -github.com/ipfs/go-ipld-cbor v0.0.5/go.mod h1:BkCduEx3XBCO6t2Sfo5BaHzuok7hbhdMm9Oh8B2Ftq4= github.com/ipfs/go-ipld-cbor v0.0.6-0.20211211231443-5d9b9e1f6fa8/go.mod h1:ssdxxaLJPXH7OjF5V4NSjBbcfh+evoR4ukuru0oPXMA= -github.com/ipfs/go-ipld-cbor v0.0.6/go.mod h1:ssdxxaLJPXH7OjF5V4NSjBbcfh+evoR4ukuru0oPXMA= github.com/ipfs/go-ipld-cbor v0.2.1 h1:H05yEJbK/hxg0uf2AJhyerBDbjOuHX4yi+1U/ogRa7E= github.com/ipfs/go-ipld-cbor v0.2.1/go.mod h1:x9Zbeq8CoE5R2WicYgBMcr/9mnkQ0lHddYWJP2sMV3A= github.com/ipfs/go-ipld-format v0.0.1/go.mod h1:kyJtbkDALmFHv3QR6et67i35QzO3S0dCDnkOJhcZkms= @@ -749,7 +715,6 @@ github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= github.com/ipfs/go-log/v2 v2.0.1/go.mod h1:O7P1lJt27vWHhOwQmcFEvlmo49ry2VY2+JfBWFaa9+0= github.com/ipfs/go-log/v2 v2.0.5/go.mod h1:eZs4Xt4ZUJQFM3DlanGhy7TkwwawCZcSByscwkWG+dw= -github.com/ipfs/go-log/v2 v2.1.2-0.20200626104915-0016c0b4b3e4/go.mod h1:2v2nsGfZsvvAJz13SyFzf9ObaqwHiHxsPLEHntrv9KM= github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= github.com/ipfs/go-log/v2 v2.8.2 h1:nVG4nNHUwwI/sTs9Bi5iE8sXFQwXs3AjkkuWhg7+Y2I= github.com/ipfs/go-log/v2 v2.8.2/go.mod h1:UhIYAwMV7Nb4ZmihUxfIRM2Istw/y9cAk3xaK+4Zs2c= @@ -831,13 +796,10 @@ github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uT github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/jsign/go-filsigner v0.4.1 h1:3OfSU851aMRmJCcgZrnepIEUJ1sfYV3/+lRVMspOEiA= -github.com/jsign/go-filsigner v0.4.1/go.mod h1:1vBLymj4qKJt1Ixkna83Peof9kjY/TA5uCJ/7167iuk= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -852,11 +814,6 @@ github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaR github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= -github.com/kilic/bls12-381 v0.0.0-20200607163746-32e1441c8a9f/go.mod h1:XXfR6YFCRSrkEXbNlIyDsgXVNJWVUV30m/ebkVy9n6s= -github.com/kilic/bls12-381 v0.0.0-20200731194930-64c428e1bff5/go.mod h1:XXfR6YFCRSrkEXbNlIyDsgXVNJWVUV30m/ebkVy9n6s= -github.com/kilic/bls12-381 v0.0.0-20200820230200-6b2c19996391/go.mod h1:XXfR6YFCRSrkEXbNlIyDsgXVNJWVUV30m/ebkVy9n6s= -github.com/kilic/bls12-381 v0.1.1-0.20220929213557-ca162e8a70f4 h1:xWK4TZ4bRL05WQUU/3x6TG1l+IYAqdXpAeSLt/zZJc4= -github.com/kilic/bls12-381 v0.1.1-0.20220929213557-ca162e8a70f4/go.mod h1:tlkavyke+Ac7h8R3gZIjI5LKBcvMlSWnXNMgT3vZXo8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -864,7 +821,6 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= -github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -961,7 +917,6 @@ github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8Rv github.com/minio/sha256-simd v0.0.0-20190131020904-2d45a736cd16/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U= github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= -github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -1018,13 +973,11 @@ github.com/multiformats/go-multihash v0.0.9/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa github.com/multiformats/go-multihash v0.0.10/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= github.com/multiformats/go-multihash v0.0.14/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= -github.com/multiformats/go-multihash v0.0.15/go.mod h1:D6aZrWNLFTV/ynMpKsNtB40mJzmCl4jb1alC0OvHiHg= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= -github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -1277,8 +1230,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= -github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/swaggo/echo-swagger v1.4.0 h1:RCxLKySw1SceHLqnmc41pKyiIeE+OiD7NSI7FUOBlLo= github.com/swaggo/echo-swagger v1.4.0/go.mod h1:Wh3VlwjZGZf/LH0s81tz916JokuPG7y/ZqaqnckYqoQ= github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= @@ -1291,7 +1244,6 @@ github.com/t3rm1n4l/go-mega v0.0.0-20250926104142-ccb8d3498e6c h1:BLopNCyqewbE8+ github.com/t3rm1n4l/go-mega v0.0.0-20250926104142-ccb8d3498e6c/go.mod h1:ykucQyiE9Q2qx1wLlEtZkkNn1IURib/2O+Mvd25i1Fo= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= @@ -1331,7 +1283,6 @@ github.com/whyrusleeping/cbor-gen v0.0.0-20200810223238-211df3b9e24c/go.mod h1:f github.com/whyrusleeping/cbor-gen v0.0.0-20200812213548-958ddffe352c/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= github.com/whyrusleeping/cbor-gen v0.0.0-20200826160007-0b9f6c5fb163/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= github.com/whyrusleeping/cbor-gen v0.0.0-20210118024343-169e9d70c0c2/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= -github.com/whyrusleeping/cbor-gen v0.0.0-20210303213153-67a261a1d291/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= github.com/whyrusleeping/cbor-gen v0.3.2-0.20250409092040-76796969edea h1:/uOIA87OS8z0NyGuTXnWq7Z4qCzsNrUh1AojK943zQE= github.com/whyrusleeping/cbor-gen v0.3.2-0.20250409092040-76796969edea/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f h1:jQa4QT2UP9WYv2nzyawpKMOCl+Z/jW7djv2/J50lj9E= @@ -1383,13 +1334,6 @@ gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRyS gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= -go.dedis.ch/fixbuf v1.0.3 h1:hGcV9Cd/znUxlusJ64eAlExS+5cJDIyTyEG+otu5wQs= -go.dedis.ch/fixbuf v1.0.3/go.mod h1:yzJMt34Wa5xD37V5RTdmp38cz3QhMagdGoem9anUalw= -go.dedis.ch/kyber/v3 v3.0.4/go.mod h1:OzvaEnPvKlyrWyp3kGXlFdp7ap1VC6RkZDTaPikqhsQ= -go.dedis.ch/kyber/v3 v3.0.9/go.mod h1:rhNjUUg6ahf8HEg5HUvVBYoWY4boAafX8tYxX+PS+qg= -go.dedis.ch/protobuf v1.0.5/go.mod h1:eIV4wicvi6JK0q/QnfIEGeSFNG0ZeB24kzut5+HaRLo= -go.dedis.ch/protobuf v1.0.7/go.mod h1:pv5ysfkDX/EawiPqcW3ikOxsL5t+BqnV6xHSmE79KI4= -go.dedis.ch/protobuf v1.0.11/go.mod h1:97QR256dnkimeNdfmURz0wAMNVbd1VmLXhG1CrTYrJ4= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= @@ -1460,7 +1404,6 @@ go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEb golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -1469,15 +1412,10 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= @@ -1597,7 +1535,6 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1616,13 +1553,11 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191025090151-53bf42e6b339/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1635,13 +1570,10 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200926100807-9d91bd62050c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1666,7 +1598,6 @@ golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20251022145735-5be28d707443 h1:eE5IhBiTMPgrcTS6Mlh7IG4MdydRrXr2y60Jn/JC6kM= golang.org/x/telemetry v0.0.0-20251022145735-5be28d707443/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -1745,7 +1676,6 @@ golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200711155855-7342f9734a7d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -1877,7 +1807,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -1913,19 +1842,14 @@ kr.dev/errorfmt v0.1.1 h1:0YA5N2yV0xKxJ4eD5cX2S9wEnJHDHOZzerKbrZqtRrQ= kr.dev/errorfmt v0.1.1/go.mod h1:X5EQZa3qf6c/l1DMjhflAbKGAGvlP6/ByWnaOpfbJME= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= -modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= -modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY= modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw= -modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU= modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI= -modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= -modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8= moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= diff --git a/handler/dataprep/delete_piece_test.go b/handler/dataprep/delete_piece_test.go index 72d16bbe8..5fbad85c4 100644 --- a/handler/dataprep/delete_piece_test.go +++ b/handler/dataprep/delete_piece_test.go @@ -66,7 +66,7 @@ func TestDeletePieceHandler_DealExistsWithoutForce(t *testing.T) { require.NoError(t, db.Create(&car).Error) // Create a wallet first to satisfy FK constraint - wallet := model.Wallet{ID: "f01234", Address: "f01234"} + wallet := model.Actor{ID: "f01234", Address: "f01234"} require.NoError(t, db.Create(&wallet).Error) deal := model.Deal{ @@ -96,7 +96,7 @@ func TestDeletePieceHandler_DealExistsWithForce(t *testing.T) { require.NoError(t, db.Create(&car).Error) // Create a wallet first to satisfy FK constraint - wallet := model.Wallet{ID: "f01234", Address: "f01234"} + wallet := model.Actor{ID: "f01234", Address: "f01234"} require.NoError(t, db.Create(&wallet).Error) deal := model.Deal{ diff --git a/handler/deal/interface.go b/handler/deal/interface.go index e6f92ce84..42aed347c 100644 --- a/handler/deal/interface.go +++ b/handler/deal/interface.go @@ -6,18 +6,15 @@ import ( "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/replication" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/stretchr/testify/mock" + "github.com/ybbus/jsonrpc/v3" "gorm.io/gorm" ) type Handler interface { ListHandler(ctx context.Context, db *gorm.DB, request ListDealRequest) ([]model.Deal, error) - SendManualHandler( - ctx context.Context, - db *gorm.DB, - dealMaker replication.DealMaker, - request Proposal, - ) (*model.Deal, error) + SendManualHandler(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, lotusClient jsonrpc.RPCClient, dealMaker replication.DealMaker, request Proposal) (*model.Deal, error) } type DefaultHandler struct{} @@ -35,7 +32,7 @@ func (m *MockDeal) ListHandler(ctx context.Context, db *gorm.DB, request ListDea return args.Get(0).([]model.Deal), args.Error(1) } -func (m *MockDeal) SendManualHandler(ctx context.Context, db *gorm.DB, dealMaker replication.DealMaker, request Proposal) (*model.Deal, error) { - args := m.Called(ctx, db, dealMaker, request) +func (m *MockDeal) SendManualHandler(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, lotusClient jsonrpc.RPCClient, dealMaker replication.DealMaker, request Proposal) (*model.Deal, error) { + args := m.Called(ctx, db, ks, lotusClient, dealMaker, request) return args.Get(0).(*model.Deal), args.Error(1) } diff --git a/handler/deal/list_test.go b/handler/deal/list_test.go index 96005ebfe..4592fe8f1 100644 --- a/handler/deal/list_test.go +++ b/handler/deal/list_test.go @@ -12,9 +12,11 @@ import ( func TestListHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&model.Preparation{ + err := db.Create(&model.Actor{ID: "f01", Address: "f01"}).Error + require.NoError(t, err) + err = db.Create(&model.Preparation{ Wallets: []model.Wallet{{ - ID: "f01", + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, SourceStorages: []model.Storage{{ Name: "storage", diff --git a/handler/deal/schedule/create_test.go b/handler/deal/schedule/create_test.go index 4e1da85fe..7c3e8022b 100644 --- a/handler/deal/schedule/create_test.go +++ b/handler/deal/schedule/create_test.go @@ -190,7 +190,7 @@ func TestCreateHandler_InvalidProvider(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", + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) @@ -207,7 +207,7 @@ func TestCreateHandler_DealSizeNotSetForCron(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", + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) @@ -227,7 +227,7 @@ func TestCreateHandler_ScheduleDealSizeSetForNonCron(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", + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) @@ -248,7 +248,7 @@ func TestCreateHandler_Success(t *testing.T) { err := db.Create(&model.Preparation{ Name: "name", Wallets: []model.Wallet{{ - ID: "f01", + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) diff --git a/handler/deal/schedule/list_test.go b/handler/deal/schedule/list_test.go index e0c906f2e..986753520 100644 --- a/handler/deal/schedule/list_test.go +++ b/handler/deal/schedule/list_test.go @@ -14,7 +14,7 @@ func TestListHandler(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", + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) diff --git a/handler/deal/schedule/pause_test.go b/handler/deal/schedule/pause_test.go index 17090f824..155fa20a7 100644 --- a/handler/deal/schedule/pause_test.go +++ b/handler/deal/schedule/pause_test.go @@ -15,7 +15,7 @@ func TestPauseHandler(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", + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) diff --git a/handler/deal/schedule/remove_test.go b/handler/deal/schedule/remove_test.go index afd825d31..c7ac30baa 100644 --- a/handler/deal/schedule/remove_test.go +++ b/handler/deal/schedule/remove_test.go @@ -14,9 +14,11 @@ import ( func TestRemoveSchedule_Success(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&model.Preparation{ + err := db.Create(&model.Actor{ID: "f01", Address: "f01"}).Error + require.NoError(t, err) + err = db.Create(&model.Preparation{ Wallets: []model.Wallet{{ - ID: "f01", + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) @@ -57,7 +59,7 @@ func TestRemoveSchedule_StillActive(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", + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) diff --git a/handler/deal/schedule/resume_test.go b/handler/deal/schedule/resume_test.go index 85d87b957..e270d85b8 100644 --- a/handler/deal/schedule/resume_test.go +++ b/handler/deal/schedule/resume_test.go @@ -15,7 +15,7 @@ func TestResumeHandler(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", + Address: "f01", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) diff --git a/handler/deal/send-manual.go b/handler/deal/send-manual.go index 498cc7bb8..246e839ca 100644 --- a/handler/deal/send-manual.go +++ b/handler/deal/send-manual.go @@ -9,10 +9,14 @@ import ( "github.com/cockroachdb/errors" "github.com/data-preservation-programs/singularity/handler/handlererror" + "github.com/data-preservation-programs/singularity/handler/wallet" "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/replication" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/dustin/go-humanize" + "github.com/filecoin-project/go-state-types/crypto" "github.com/ipfs/go-cid" + "github.com/ybbus/jsonrpc/v3" "gorm.io/gorm" ) @@ -66,15 +70,17 @@ func argToDuration(s string) (time.Duration, error) { func (DefaultHandler) SendManualHandler( ctx context.Context, db *gorm.DB, + ks keystore.KeyStore, + lotusClient jsonrpc.RPCClient, dealMaker replication.DealMaker, request Proposal, ) (*model.Deal, error) { db = db.WithContext(ctx) - // Get the wallet object - wallet := model.Wallet{} - err := db.Where("id = ? OR address = ?", request.ClientAddress, request.ClientAddress).First(&wallet).Error + // find wallet by address, then lazily resolve actor for the deal proposal + var walletObj model.Wallet + err := db.Where("address = ?", request.ClientAddress).First(&walletObj).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(handlererror.ErrNotFound, "client address %s not found", request.ClientAddress) + return nil, errors.Wrapf(handlererror.ErrNotFound, "wallet %s not found", request.ClientAddress) } if err != nil { return nil, errors.WithStack(err) @@ -136,7 +142,17 @@ func (DefaultHandler) SendManualHandler( Duration: duration, } - dealModel, err := dealMaker.MakeDeal(ctx, wallet, car, dealConfig) + // resolve actor lazily — only makes RPC call if ActorID not yet linked + actor, err := wallet.GetOrCreateActor(ctx, db, lotusClient, &walletObj) + if err != nil { + return nil, errors.Wrapf(err, "failed to resolve actor for wallet %s", walletObj.Address) + } + + signer := replication.ProposalSigner(func(msg []byte) (*crypto.Signature, error) { + return wallet.SignWithWallet(ks, walletObj, msg) + }) + + dealModel, err := dealMaker.MakeDeal(ctx, *actor, car, dealConfig, signer) if err != nil { return nil, errors.WithStack(err) } diff --git a/handler/deal/send-manual_test.go b/handler/deal/send-manual_test.go index 0d55f66a3..08318b91f 100644 --- a/handler/deal/send-manual_test.go +++ b/handler/deal/send-manual_test.go @@ -18,8 +18,8 @@ type MockDealMaker struct { mock.Mock } -func (m *MockDealMaker) MakeDeal(ctx context.Context, walletObj model.Wallet, car model.Car, dealConfig replication.DealConfig) (*model.Deal, error) { - args := m.Called(ctx, walletObj, car, dealConfig) +func (m *MockDealMaker) MakeDeal(ctx context.Context, actorObj model.Actor, car model.Car, dealConfig replication.DealConfig, signer replication.ProposalSigner) (*model.Deal, error) { + args := m.Called(ctx, actorObj, car, dealConfig) return args.Get(0).(*model.Deal), args.Error(1) } @@ -39,176 +39,128 @@ var proposal = Proposal{ FileSize: 1000, } -func TestSendManualHandler_WalletNotFound(t *testing.T) { - wallet := model.Wallet{ - ID: "f09999", - Address: "f10000", +// creates a wallet (and optionally an actor) that matches proposal.ClientAddress +func createTestWalletAndActor(t *testing.T, db *gorm.DB, withActor bool) { + t.Helper() + actorID := "f01000" + if withActor { + require.NoError(t, db.Create(&model.Actor{ID: actorID, Address: "f01000"}).Error) + } + w := model.Wallet{ + Address: "f01000", KeyPath: "/tmp/key-manual", KeyStore: "local", + } + if withActor { + w.ActorID = &actorID } + require.NoError(t, db.Create(&w).Error) +} +func TestSendManualHandler_WalletNotFound(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error - require.NoError(t, err) - mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, proposal) + mockDealMaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, proposal) require.ErrorIs(t, err, handlererror.ErrNotFound) - require.ErrorContains(t, err, "client address") + require.ErrorContains(t, err, "wallet") }) } func TestSendManualHandler_InvalidPieceCID(t *testing.T) { - wallet := model.Wallet{ - ID: "f01000", - Address: "f10000", - } - testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error - require.NoError(t, err) - + createTestWalletAndActor(t, db, false) mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.PieceCID = "bad" - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, badProposal) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid piece CID") }) } func TestSendManualHandler_InvalidPieceCID_NOTCOMMP(t *testing.T) { - wallet := model.Wallet{ - ID: "f01000", - Address: "f10000", - } - testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error - require.NoError(t, err) - + createTestWalletAndActor(t, db, false) mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.PieceCID = proposal.RootCID - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, badProposal) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "must be commp") }) } func TestSendManualHandler_InvalidPieceSize(t *testing.T) { - wallet := model.Wallet{ - ID: "f01000", - Address: "f10000", - } - testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error - require.NoError(t, err) - + createTestWalletAndActor(t, db, false) mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.PieceSize = "aaa" - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, badProposal) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid piece size") }) } func TestSendManualHandler_InvalidPieceSize_NotPowerOfTwo(t *testing.T) { - wallet := model.Wallet{ - ID: "f01000", - Address: "f10000", - } - testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error - require.NoError(t, err) - + createTestWalletAndActor(t, db, false) mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.PieceSize = "31GiB" - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, badProposal) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "must be a power of 2") }) } func TestSendManualHandler_InvalidRootCID(t *testing.T) { - wallet := model.Wallet{ - ID: "f01000", - Address: "f10000", - } - testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error - require.NoError(t, err) - + createTestWalletAndActor(t, db, false) mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.RootCID = "xxxx" - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, badProposal) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid root CID") }) } func TestSendManualHandler_InvalidDuration(t *testing.T) { - wallet := model.Wallet{ - ID: "f01000", - Address: "f10000", - } - testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error - require.NoError(t, err) - + createTestWalletAndActor(t, db, false) mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.Duration = "xxxx" - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, badProposal) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid duration") }) } func TestSendManualHandler_InvalidStartDelay(t *testing.T) { - wallet := model.Wallet{ - ID: "f01000", - Address: "f10000", - } - testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error - require.NoError(t, err) - + createTestWalletAndActor(t, db, false) mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, mock.Anything).Return(&model.Deal{}, nil) badProposal := proposal badProposal.StartDelay = "xxxx" - _, err = Default.SendManualHandler(ctx, db, mockDealMaker, badProposal) + _, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, badProposal) require.ErrorIs(t, err, handlererror.ErrInvalidParameter) require.ErrorContains(t, err, "invalid start delay") }) } func TestSendManualHandler(t *testing.T) { - wallet := model.Wallet{ - ID: "f01000", - Address: "f10000", + actorID := "f01000" + actor := model.Actor{ + ID: actorID, + Address: "f01000", } testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&wallet).Error - require.NoError(t, err) + createTestWalletAndActor(t, db, true) mockDealMaker := new(MockDealMaker) - mockDealMaker.On("MakeDeal", ctx, wallet, mock.Anything, replication.DealConfig{ + mockDealMaker.On("MakeDeal", mock.Anything, actor, mock.Anything, replication.DealConfig{ Provider: proposal.ProviderID, StartDelay: 24 * time.Hour, Duration: 2400 * time.Hour, @@ -221,7 +173,8 @@ func TestSendManualHandler(t *testing.T) { PricePerGB: proposal.PricePerGB, PricePerGBEpoch: proposal.PricePerGBEpoch, }).Return(&model.Deal{}, nil) - resp, err := Default.SendManualHandler(ctx, db, mockDealMaker, proposal) + // lotusClient is nil — GetOrCreateActor won't call lotus because ActorID is already set + resp, err := Default.SendManualHandler(ctx, db, nil, nil, mockDealMaker, proposal) mockDealMaker.AssertExpectations(t) require.NoError(t, err) require.NotNil(t, resp) diff --git a/handler/file/deals_test.go b/handler/file/deals_test.go index 63895340d..84ea2c119 100644 --- a/handler/file/deals_test.go +++ b/handler/file/deals_test.go @@ -77,13 +77,13 @@ func TestGetFileDealsHandler(t *testing.T) { deals := []model.Deal{{ PieceCID: model.CID(testCid1), - Wallet: &model.Wallet{}, + Actor: &model.Actor{}, }, { PieceCID: model.CID(testCid2), - Wallet: &model.Wallet{}, + Actor: &model.Actor{}, }, { PieceCID: model.CID(testCid2), - Wallet: &model.Wallet{}, + Actor: &model.Actor{}, }} err = db.Create(deals).Error require.NoError(t, err) diff --git a/handler/file/retrieve_test.go b/handler/file/retrieve_test.go index f214fa819..8114b50ce 100644 --- a/handler/file/retrieve_test.go +++ b/handler/file/retrieve_test.go @@ -144,7 +144,7 @@ func TestRetrieveFileHandler(t *testing.T) { State: model.DealActive, PieceCID: model.CID(testCid), Provider: "apples" + strconv.Itoa(i), - Wallet: &model.Wallet{}, + Actor: &model.Actor{}, } err = db.Create(&deal).Error require.NoError(t, err) @@ -158,7 +158,7 @@ func TestRetrieveFileHandler(t *testing.T) { State: state, PieceCID: model.CID(testCid), Provider: "oranges" + strconv.Itoa(i), - Wallet: &model.Wallet{}, + Actor: &model.Actor{}, } err = db.Create(&deal).Error require.NoError(t, err) @@ -489,7 +489,7 @@ func BenchmarkFilecoinRetrieve(b *testing.B) { State: model.DealActive, PieceCID: model.CID(testCid), Provider: "apples" + strconv.Itoa(i), - Wallet: &model.Wallet{}, + Actor: &model.Actor{}, } err = db.Create(&deal).Error require.NoError(b, err) @@ -502,7 +502,7 @@ func BenchmarkFilecoinRetrieve(b *testing.B) { State: state, PieceCID: model.CID(testCid), Provider: "oranges" + strconv.Itoa(i), - Wallet: &model.Wallet{}, + Actor: &model.Actor{}, } err = db.Create(&deal).Error require.NoError(b, err) diff --git a/handler/wallet/attach.go b/handler/wallet/attach.go index e2b374bf6..1fd6ac58d 100644 --- a/handler/wallet/attach.go +++ b/handler/wallet/attach.go @@ -2,6 +2,7 @@ package wallet import ( "context" + "strconv" "github.com/cockroachdb/errors" "github.com/data-preservation-programs/singularity/database" @@ -10,37 +11,32 @@ import ( "gorm.io/gorm" ) -// AttachHandler associates a wallet with a specific preparation based on given preparationID and wallet address or ID. -// -// Parameters: -// - ctx: The context for database transactions and other operations. -// - db: A pointer to the gorm.DB instance representing the database connection. -// - preparationID: The ID or name of the preparation to which the wallet will be attached. -// - wallet: The address or ID of the wallet to be attached to the preparation. -// -// Returns: -// - A pointer to the updated Preparation instance. -// - An error, if any occurred during the association operation. +// attaches wallet to preparation for deal-making +// accepts wallet address or wallet ID func (DefaultHandler) AttachHandler( ctx context.Context, db *gorm.DB, preparationID string, - wallet string, + walletAddressOrID string, ) (*model.Preparation, error) { db = db.WithContext(ctx) var preparation model.Preparation err := preparation.FindByIDOrName(db, preparationID, "SourceStorages", "OutputStorages", "Wallets") if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(handlererror.ErrNotFound, "preparation %d not found", preparationID) + return nil, errors.Wrapf(handlererror.ErrNotFound, "preparation %s not found", preparationID) } if err != nil { return nil, errors.WithStack(err) } var w model.Wallet - err = db.Where("address = ? OR id = ?", wallet, wallet).First(&w).Error + q := db.Where("address = ?", walletAddressOrID) + if id, parseErr := strconv.ParseUint(walletAddressOrID, 10, 32); parseErr == nil { + q = db.Where("address = ? OR id = ?", walletAddressOrID, id) + } + err = q.First(&w).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(handlererror.ErrNotFound, "wallet %s not found", wallet) + return nil, errors.Wrapf(handlererror.ErrNotFound, "wallet %s not found", walletAddressOrID) } if err != nil { return nil, errors.WithStack(err) diff --git a/handler/wallet/attach_test.go b/handler/wallet/attach_test.go index 1a0ee203b..847f3e35e 100644 --- a/handler/wallet/attach_test.go +++ b/handler/wallet/attach_test.go @@ -14,14 +14,14 @@ import ( func TestAttachHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { err := db.Create(&model.Wallet{ - ID: "test", + Address: "f0test", KeyPath: "/tmp/key", KeyStore: "local", }).Error require.NoError(t, err) err = db.Create(&model.Preparation{}).Error require.NoError(t, err) t.Run("preparation not found", func(t *testing.T) { - _, err := Default.AttachHandler(ctx, db, "2", "test") + _, err := Default.AttachHandler(ctx, db, "2", "f0test") require.ErrorIs(t, err, handlererror.ErrNotFound) }) @@ -31,7 +31,7 @@ func TestAttachHandler(t *testing.T) { }) t.Run("success", func(t *testing.T) { - preparation, err := Default.AttachHandler(ctx, db, "1", "test") + preparation, err := Default.AttachHandler(ctx, db, "1", "f0test") require.NoError(t, err) require.Len(t, preparation.Wallets, 1) }) diff --git a/handler/wallet/detach.go b/handler/wallet/detach.go index 7dee510fb..2f3f5356a 100644 --- a/handler/wallet/detach.go +++ b/handler/wallet/detach.go @@ -2,6 +2,7 @@ package wallet import ( "context" + "fmt" "github.com/cockroachdb/errors" "github.com/data-preservation-programs/singularity/database" @@ -11,22 +12,13 @@ import ( "gorm.io/gorm" ) -// DetachHandler removes the association of a wallet from a specific preparation based on the given preparationID and wallet address or ID. -// -// Parameters: -// - ctx: The context for database transactions and other operations. -// - db: A pointer to the gorm.DB instance representing the database connection. -// - preparationID: The ID or name of the preparation from which the wallet will be removed. -// - wallet: The address or ID of the wallet to be removed from the preparation. -// -// Returns: -// - A pointer to the updated Preparation instance. -// - An error, if any occurred during the removal operation. +// detaches wallet from preparation +// accepts wallet address or ID func (DefaultHandler) DetachHandler( ctx context.Context, db *gorm.DB, preparationID string, - wallet string, + walletAddressOrID string, ) (*model.Preparation, error) { db = db.WithContext(ctx) var preparation model.Preparation @@ -39,10 +31,10 @@ func (DefaultHandler) DetachHandler( } found, err := underscore.Find(preparation.Wallets, func(w model.Wallet) bool { - return w.ID == wallet || w.Address == wallet + return w.Address == walletAddressOrID || fmt.Sprint(w.ID) == walletAddressOrID }) if err != nil { - return nil, errors.Wrapf(handlererror.ErrNotFound, "wallet %s not attached to preparation %d", wallet, preparationID) + return nil, errors.Wrapf(handlererror.ErrNotFound, "wallet %s not attached to preparation %s", walletAddressOrID, preparationID) } err = database.DoRetry(ctx, func() error { diff --git a/handler/wallet/detach_test.go b/handler/wallet/detach_test.go index 268da587c..1d9435482 100644 --- a/handler/wallet/detach_test.go +++ b/handler/wallet/detach_test.go @@ -15,13 +15,13 @@ func TestDetachHandler(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: "test", + Address: "f0test", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) t.Run("preparation not found", func(t *testing.T) { - _, err := Default.DetachHandler(ctx, db, "2", "test") + _, err := Default.DetachHandler(ctx, db, "2", "f0test") require.ErrorIs(t, err, handlererror.ErrNotFound) }) @@ -31,7 +31,7 @@ func TestDetachHandler(t *testing.T) { }) t.Run("success", func(t *testing.T) { - preparation, err := Default.DetachHandler(ctx, db, "1", "test") + preparation, err := Default.DetachHandler(ctx, db, "1", "f0test") require.NoError(t, err) require.Len(t, preparation.Wallets, 0) }) diff --git a/handler/wallet/import.go b/handler/wallet/import.go deleted file mode 100644 index 3775d8cc2..000000000 --- a/handler/wallet/import.go +++ /dev/null @@ -1,92 +0,0 @@ -package wallet - -import ( - "context" - - "github.com/cockroachdb/errors" - "github.com/data-preservation-programs/singularity/database" - "github.com/data-preservation-programs/singularity/handler/handlererror" - "github.com/data-preservation-programs/singularity/model" - "github.com/data-preservation-programs/singularity/util" - "github.com/filecoin-project/go-address" - "github.com/ipfs/go-log/v2" - "github.com/jsign/go-filsigner/wallet" - "github.com/ybbus/jsonrpc/v3" - "gorm.io/gorm" -) - -var logger = log.Logger("singularity/handler/wallet") - -type ImportRequest struct { - PrivateKey string `json:"privateKey"` // This is the exported private key from lotus wallet export -} - -// @ID ImportWallet -// @Summary Import a private key -// @Tags Wallet -// @Accept json -// @Produce json -// @Param request body ImportRequest true "Request body" -// @Success 200 {object} model.Wallet -// @Failure 400 {object} api.HTTPError -// @Failure 500 {object} api.HTTPError -// @Router /wallet [post] -func _() {} - -// ImportHandler imports a wallet into the system using a given private key. It first verifies the private key's -// validity by generating its associated public address. It then checks for the existence of this address in the -// Lotus system using the provided RPC client. After confirming the actor ID from the Lotus system, it creates a -// new wallet record in the local database. -// -// Parameters: -// - ctx: The context for database transactions and other operations. -// - db: A pointer to the gorm.DB instance representing the database connection. -// - lotusClient: The RPC client used to interact with a Lotus node for actor lookup. -// - request: The request containing the private key for the wallet import operation. -// -// Returns: -// - A pointer to the created Wallet model if successful. -// - An error, if any occurred during the operation. -func (DefaultHandler) ImportHandler( - ctx context.Context, - db *gorm.DB, - lotusClient jsonrpc.RPCClient, - request ImportRequest, -) (*model.Wallet, error) { - db = db.WithContext(ctx) - addr, err := wallet.PublicKey(request.PrivateKey) - if err != nil { - logger.Errorw("failed to instantiate wallet address from private key", "err", err) - return nil, errors.Wrap(handlererror.ErrInvalidParameter, "invalid private key") - } - - var result string - err = lotusClient.CallFor(ctx, &result, "Filecoin.StateLookupID", addr.String(), nil) - if err != nil { - logger.Errorw("failed to lookup state for wallet address", "addr", addr, "err", err) - return nil, errors.Join(handlererror.ErrInvalidParameter, errors.Wrap(err, "failed to lookup actor ID")) - } - - _, err = address.NewFromString(result) - if err != nil { - return nil, errors.Wrap(handlererror.ErrInvalidParameter, "invalid actor ID") - } - - wallet := model.Wallet{ - ID: result, - Address: result[:1] + addr.String()[1:], - PrivateKey: request.PrivateKey, - } - - err = database.DoRetry(ctx, func() error { - return db.Create(&wallet).Error - }) - if util.IsDuplicateKeyError(err) { - return nil, errors.Wrap(handlererror.ErrDuplicateRecord, "wallet already imported") - } - if err != nil { - return nil, errors.WithStack(err) - } - - return &wallet, nil -} diff --git a/handler/wallet/import_keystore.go b/handler/wallet/import_keystore.go new file mode 100644 index 000000000..e6450954a --- /dev/null +++ b/handler/wallet/import_keystore.go @@ -0,0 +1,102 @@ +package wallet + +import ( + "context" + "os" + "path/filepath" + + "github.com/cockroachdb/errors" + "github.com/data-preservation-programs/singularity/database" + "github.com/data-preservation-programs/singularity/handler/handlererror" + "github.com/data-preservation-programs/singularity/model" + "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/keystore" + "github.com/ipfs/go-log/v2" + "gorm.io/gorm" +) + +var logger = log.Logger("singularity/handler/wallet") + +type ImportKeystoreRequest struct { + PrivateKey string `json:"privateKey"` // lotus wallet export format + Name string `json:"name"` // optional human-readable name +} + +// @ID ImportWallet +// @Summary Import a private key +// @Tags Wallet +// @Accept json +// @Produce json +// @Param request body ImportKeystoreRequest true "Request body" +// @Success 200 {object} model.Wallet +// @Failure 400 {object} api.HTTPError +// @Failure 500 {object} api.HTTPError +// @Router /wallet [post] +func _() {} + +// imports wallet by saving private key to keystore and creating wallet record +// does not require actor to exist on-chain - wallet can be imported offline +// uses external keystore instead of storing keys in database +func (DefaultHandler) ImportKeystoreHandler( + ctx context.Context, + db *gorm.DB, + ks keystore.KeyStore, + request ImportKeystoreRequest, +) (*model.Wallet, error) { + db = db.WithContext(ctx) + + // validate key before touching keystore: derive address to distinguish + // bad input (400) from keystore I/O failures (500) + addr, err := keystore.AddressFromExport(request.PrivateKey) + if err != nil { + return nil, errors.Wrap(handlererror.ErrInvalidParameter, err.Error()) + } + + keyPath, _, err := ks.Put(request.PrivateKey) + if err != nil { + logger.Errorw("failed to save key to keystore", "err", err) + return nil, errors.WithStack(err) + } + + logger.Infow("saved key to keystore", "address", addr.String(), "path", keyPath) + + walletRecord := model.Wallet{ + KeyPath: keyPath, + KeyStore: "local", + Address: addr.String(), + Name: request.Name, + ActorID: nil, // populated lazily when needed + } + + err = database.DoRetry(ctx, func() error { + return db.Create(&walletRecord).Error + }) + + if util.IsDuplicateKeyError(err) { + // don't delete the key file — it belongs to the existing wallet record + return nil, errors.Wrap(handlererror.ErrDuplicateRecord, "wallet already imported") + } + + if err != nil { + ks.Delete(keyPath) // cleanup only for non-duplicate failures + return nil, errors.WithStack(err) + } + + logger.Infow("imported wallet", "id", walletRecord.ID, "address", addr.String()) + + return &walletRecord, nil +} + +// returns default keystore directory path +// TODO: make configurable via config file +func GetKeystoreDir() string { + if dir := os.Getenv("SINGULARITY_KEYSTORE"); dir != "" { + return dir + } + + home, err := os.UserHomeDir() + if err != nil { + return filepath.Join(".", ".singularity", "keystore") + } + return filepath.Join(home, ".singularity", "keystore") +} diff --git a/handler/wallet/import_test.go b/handler/wallet/import_test.go index 614de4d8b..8334d9610 100644 --- a/handler/wallet/import_test.go +++ b/handler/wallet/import_test.go @@ -2,49 +2,121 @@ package wallet import ( "context" + "os" "testing" - "time" + "github.com/cockroachdb/errors" "github.com/data-preservation-programs/singularity/handler/handlererror" - "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/data-preservation-programs/singularity/util/testutil" "github.com/stretchr/testify/require" "gorm.io/gorm" ) -func TestImportHandler(t *testing.T) { - testutil.SkipIfNotExternalAPI(t) +func TestImportKeystoreHandler_Success(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - lotusClient := util.NewLotusClient(testutil.TestLotusAPI, "") - - t.Run("success", func(t *testing.T) { - w, err := Default.ImportHandler(ctx, db, lotusClient, ImportRequest{ - PrivateKey: testutil.TestPrivateKeyHex, - }) - require.NoError(t, err) - require.Equal(t, testutil.TestWalletAddr, w.Address) - - _, err = Default.ImportHandler(ctx, db, lotusClient, ImportRequest{ - PrivateKey: testutil.TestPrivateKeyHex, - }) - require.ErrorIs(t, err, handlererror.ErrDuplicateRecord) + ks, err := keystore.NewLocalKeyStore(t.TempDir()) + require.NoError(t, err) + + h := DefaultHandler{} + w, err := h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ + PrivateKey: testutil.TestPrivateKeyHex, + Name: "test-wallet", + }) + require.NoError(t, err) + require.NotNil(t, w) + require.Equal(t, testutil.TestWalletAddr, w.Address) + require.Equal(t, "test-wallet", w.Name) + require.Equal(t, "local", w.KeyStore) + require.NotEmpty(t, w.KeyPath) + require.Nil(t, w.ActorID) + require.True(t, ks.Has(w.KeyPath)) + }) +} + +func TestImportKeystoreHandler_NoName(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + ks, err := keystore.NewLocalKeyStore(t.TempDir()) + require.NoError(t, err) + + h := DefaultHandler{} + w, err := h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ + PrivateKey: testutil.TestPrivateKeyHex, + }) + require.NoError(t, err) + require.Equal(t, "", w.Name) + require.Equal(t, testutil.TestWalletAddr, w.Address) + }) +} + +func TestImportKeystoreHandler_Duplicate(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + ks, err := keystore.NewLocalKeyStore(t.TempDir()) + require.NoError(t, err) + + h := DefaultHandler{} + w, err := h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ + PrivateKey: testutil.TestPrivateKeyHex, }) + require.NoError(t, err) - t.Run("invalid key", func(t *testing.T) { - _, err := Default.ImportHandler(ctx, db, lotusClient, ImportRequest{ - PrivateKey: "xxxx", - }) - require.ErrorIs(t, err, handlererror.ErrInvalidParameter) + // second import of same key should fail but not delete the key file + _, err = h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ + PrivateKey: testutil.TestPrivateKeyHex, }) + require.ErrorIs(t, err, handlererror.ErrDuplicateRecord) + require.True(t, ks.Has(w.KeyPath), "key file must survive duplicate import") + + // original key must still be readable + key, err := ks.Get(w.KeyPath) + require.NoError(t, err) + require.Equal(t, testutil.TestPrivateKeyHex, key) + }) +} + +func TestImportKeystoreHandler_InvalidKey(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + ks, err := keystore.NewLocalKeyStore(t.TempDir()) + require.NoError(t, err) + + h := DefaultHandler{} + _, err = h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ + PrivateKey: "not-a-valid-key", + }) + require.ErrorIs(t, err, handlererror.ErrInvalidParameter) + }) +} + +func TestImportKeystoreHandler_KeystoreWriteFailure(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + dir := t.TempDir() + ks, err := keystore.NewLocalKeyStore(dir) + require.NoError(t, err) + + // make keystore directory read-only to force write failure + require.NoError(t, os.Chmod(dir, 0500)) + t.Cleanup(func() { os.Chmod(dir, 0700) }) + + h := DefaultHandler{} + _, err = h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ + PrivateKey: testutil.TestPrivateKeyHex, + }) + require.Error(t, err) + // must NOT be a 400 client error — this is a server-side I/O failure + require.False(t, errors.Is(err, handlererror.ErrInvalidParameter), + "keystore I/O failure must not be classified as invalid parameter") + }) +} + +func TestImportKeystoreHandler_EmptyKey(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + ks, err := keystore.NewLocalKeyStore(t.TempDir()) + require.NoError(t, err) - t.Run("invalid response", func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - lotusClient := util.NewLotusClient("http://127.0.0.1", "") - _, err := Default.ImportHandler(ctx, db, lotusClient, ImportRequest{ - PrivateKey: testutil.TestPrivateKeyHex, - }) - require.ErrorIs(t, err, handlererror.ErrInvalidParameter) + h := DefaultHandler{} + _, err = h.ImportKeystoreHandler(ctx, db, ks, ImportKeystoreRequest{ + PrivateKey: "", }) + require.ErrorIs(t, err, handlererror.ErrInvalidParameter) }) } diff --git a/handler/wallet/interface.go b/handler/wallet/interface.go index 163dc1c65..742efb841 100644 --- a/handler/wallet/interface.go +++ b/handler/wallet/interface.go @@ -5,12 +5,18 @@ import ( "context" "github.com/data-preservation-programs/singularity/model" + "github.com/data-preservation-programs/singularity/util/keystore" "github.com/stretchr/testify/mock" - "github.com/ybbus/jsonrpc/v3" "gorm.io/gorm" ) type Handler interface { + ImportKeystoreHandler( + ctx context.Context, + db *gorm.DB, + ks keystore.KeyStore, + request ImportKeystoreRequest, + ) (*model.Wallet, error) AttachHandler( ctx context.Context, db *gorm.DB, @@ -23,12 +29,6 @@ type Handler interface { preparation string, wallet string, ) (*model.Preparation, error) - ImportHandler( - ctx context.Context, - db *gorm.DB, - lotusClient jsonrpc.RPCClient, - request ImportRequest, - ) (*model.Wallet, error) ListHandler( ctx context.Context, db *gorm.DB, @@ -41,6 +41,7 @@ type Handler interface { RemoveHandler( ctx context.Context, db *gorm.DB, + ks keystore.KeyStore, address string, ) error } @@ -55,6 +56,11 @@ type MockWallet struct { mock.Mock } +func (m *MockWallet) ImportKeystoreHandler(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, request ImportKeystoreRequest) (*model.Wallet, error) { + args := m.Called(ctx, db, ks, request) + return args.Get(0).(*model.Wallet), args.Error(1) +} + func (m *MockWallet) AttachHandler(ctx context.Context, db *gorm.DB, preparation string, wallet string) (*model.Preparation, error) { args := m.Called(ctx, db, preparation, wallet) return args.Get(0).(*model.Preparation), args.Error(1) @@ -65,11 +71,6 @@ func (m *MockWallet) DetachHandler(ctx context.Context, db *gorm.DB, preparation return args.Get(0).(*model.Preparation), 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) -} - func (m *MockWallet) ListHandler(ctx context.Context, db *gorm.DB) ([]model.Wallet, error) { args := m.Called(ctx, db) return args.Get(0).([]model.Wallet), args.Error(1) @@ -80,7 +81,7 @@ func (m *MockWallet) ListAttachedHandler(ctx context.Context, db *gorm.DB, prepa return args.Get(0).([]model.Wallet), args.Error(1) } -func (m *MockWallet) RemoveHandler(ctx context.Context, db *gorm.DB, address string) error { - args := m.Called(ctx, db, address) +func (m *MockWallet) RemoveHandler(ctx context.Context, db *gorm.DB, ks keystore.KeyStore, address string) error { + args := m.Called(ctx, db, ks, address) return args.Error(0) } diff --git a/handler/wallet/listattached.go b/handler/wallet/listattached.go index 665ec6344..ff8f3490f 100644 --- a/handler/wallet/listattached.go +++ b/handler/wallet/listattached.go @@ -9,21 +9,6 @@ import ( "gorm.io/gorm" ) -// ListAttachedHandler fetches and returns a list of wallets associated with a given preparation, -// identified by either its ID or name. -// -// The function looks for the preparation with the specified ID or name in the database. If found, -// it retrieves all wallets associated with that preparation. If no such preparation is found, -// an error is returned. -// -// Parameters: -// - ctx: The context in which the handler function is executed, used for controlling cancellation. -// - db: A pointer to a gorm.DB object, which provides database access. -// - preparationID: The ID or name of the preparation whose attached wallets need to be fetched. -// -// Returns: -// - A slice of model.Wallet objects that are attached to the specified preparation. -// - An error if any issues arise during the process or if the preparation is not found, otherwise nil. func (DefaultHandler) ListAttachedHandler( ctx context.Context, db *gorm.DB, diff --git a/handler/wallet/listattached_test.go b/handler/wallet/listattached_test.go index 26fb17fc0..94a7321bb 100644 --- a/handler/wallet/listattached_test.go +++ b/handler/wallet/listattached_test.go @@ -15,7 +15,7 @@ func TestListAttachedHandler(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: "test", + Address: "f0test", KeyPath: "/tmp/key", KeyStore: "local", }}, }).Error require.NoError(t, err) diff --git a/handler/wallet/remove.go b/handler/wallet/remove.go index cb5ecbfd1..2369cf2e5 100644 --- a/handler/wallet/remove.go +++ b/handler/wallet/remove.go @@ -7,36 +7,42 @@ import ( "github.com/data-preservation-programs/singularity/database" "github.com/data-preservation-programs/singularity/handler/handlererror" "github.com/data-preservation-programs/singularity/model" + "github.com/data-preservation-programs/singularity/util/keystore" "gorm.io/gorm" ) -// RemoveHandler deletes a wallet from the database based on its address or ID. -// -// Parameters: -// - ctx: The context for database transactions and other operations. -// - db: A pointer to the gorm.DB instance representing the database connection. -// - address: The address or ID of the wallet to be deleted. -// -// Returns: -// - An error, if any occurred during the database deletion operation. +// removes wallet record and keystore file +// does not remove the associated actor — actors may be shared or tracked independently func (DefaultHandler) RemoveHandler( ctx context.Context, db *gorm.DB, + ks keystore.KeyStore, address string, ) error { db = db.WithContext(ctx) - var affected int64 - err := database.DoRetry(ctx, func() error { - tx := db.Where("address = ? OR id = ?", address, address).Delete(&model.Wallet{}) - affected = tx.RowsAffected - return tx.Error + var w model.Wallet + err := db.Where("address = ?", address).First(&w).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrapf(handlererror.ErrNotFound, "wallet %s not found", address) + } + if err != nil { + return errors.WithStack(err) + } + + err = database.DoRetry(ctx, func() error { + return db.Delete(&w).Error }) if err != nil { return errors.WithStack(err) } - if affected == 0 { - return errors.Wrapf(handlererror.ErrNotFound, "wallet %s not found", address) + + // best-effort keystore cleanup + if ks != nil && ks.Has(w.KeyPath) { + if delErr := ks.Delete(w.KeyPath); delErr != nil { + logger.Warnw("failed to delete key file", "path", w.KeyPath, "err", delErr) + } } + return nil } diff --git a/handler/wallet/remove_test.go b/handler/wallet/remove_test.go index dd52ecc0a..2fef0fd59 100644 --- a/handler/wallet/remove_test.go +++ b/handler/wallet/remove_test.go @@ -13,14 +13,18 @@ import ( func TestRemoveHandler(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + t.Run("not found", func(t *testing.T) { + err := Default.RemoveHandler(ctx, db, nil, "nonexistent") + require.ErrorIs(t, err, handlererror.ErrNotFound) + }) t.Run("success", func(t *testing.T) { err := db.Create(&model.Wallet{ - ID: "test", + Address: "f0remove", KeyPath: "/tmp/key-remove", KeyStore: "local", }).Error require.NoError(t, err) - err = Default.RemoveHandler(ctx, db, "test") + err = Default.RemoveHandler(ctx, db, nil, "f0remove") require.NoError(t, err) - err = Default.RemoveHandler(ctx, db, "test") + err = Default.RemoveHandler(ctx, db, nil, "f0remove") require.ErrorIs(t, err, handlererror.ErrNotFound) }) }) diff --git a/handler/wallet/sign.go b/handler/wallet/sign.go new file mode 100644 index 000000000..1d3a7bdfb --- /dev/null +++ b/handler/wallet/sign.go @@ -0,0 +1,130 @@ +package wallet + +import ( + "context" + + "github.com/cockroachdb/errors" + "github.com/data-preservation-programs/singularity/model" + "github.com/data-preservation-programs/singularity/util/keystore" + "github.com/filecoin-project/go-state-types/crypto" + "github.com/ybbus/jsonrpc/v3" + "gorm.io/gorm" +) + +// loads private key from keystore and signs a filecoin message +func SignWithWallet(ks keystore.KeyStore, wallet model.Wallet, msg []byte) (*crypto.Signature, error) { + s, err := keystore.Signer(ks, wallet) + if err != nil { + return nil, errors.Wrap(err, "failed to load signer from keystore") + } + + signature, err := s.Sign(msg) + if err != nil { + return nil, errors.Wrap(err, "failed to sign message") + } + + logger.Debugw("signed message", "address", wallet.Address, "msgLen", len(msg)) + return signature, nil +} + +// lazy actor lookup and creation for a wallet +// workflow: import wallet offline → fund externally → first deal queries on-chain actor +// returns existing actor if wallet.ActorID already set, otherwise queries lotus and creates record +func GetOrCreateActor( + ctx context.Context, + db *gorm.DB, + lotusClient jsonrpc.RPCClient, + wallet *model.Wallet, +) (*model.Actor, error) { + db = db.WithContext(ctx) + + // return existing actor if already linked + if wallet.ActorID != nil { + var actor model.Actor + err := db.First(&actor, "id = ?", *wallet.ActorID).Error + if err != nil { + return nil, errors.Wrapf(err, "actor %s not found in database", *wallet.ActorID) + } + logger.Debugw("wallet already linked to actor", "walletID", wallet.ID, "actorID", actor.ID) + return &actor, nil + } + + // query lotus for on-chain actor + logger.Infow("looking up actor on-chain", "address", wallet.Address) + + var actorID string + err := lotusClient.CallFor(ctx, &actorID, "Filecoin.StateLookupID", wallet.Address, nil) + if err != nil { + logger.Warnw("actor not found on-chain", "address", wallet.Address, "err", err) + return nil, errors.Wrapf(err, "actor for address %s not found on-chain - wallet may need funding", wallet.Address) + } + + logger.Infow("found actor on-chain", "address", wallet.Address, "actorID", actorID) + + // check if actor already exists in database + var existingActor model.Actor + err = db.First(&existingActor, "id = ?", actorID).Error + if err == nil { + // actor exists - verify not linked to different wallet + var otherWallet model.Wallet + err = db.Where("actor_id = ?", actorID).First(&otherWallet).Error + if err == nil && otherWallet.ID != wallet.ID { + logger.Warnw("actor already linked to different wallet", + "actorID", actorID, + "existingWalletID", otherWallet.ID, + "newWalletID", wallet.ID) + return nil, errors.Errorf("actor %s already linked to wallet %d", actorID, otherWallet.ID) + } + + // link to this wallet + wallet.ActorID = &actorID + err = db.Save(wallet).Error + if err != nil { + return nil, errors.Wrap(err, "failed to link wallet to existing actor") + } + + logger.Infow("linked wallet to existing actor", "walletID", wallet.ID, "actorID", actorID) + return &existingActor, nil + } + + // create new actor record + newActor := model.Actor{ + ID: actorID, + Address: wallet.Address, + } + + err = db.Create(&newActor).Error + if err != nil { + return nil, errors.Wrap(err, "failed to create actor record") + } + + // link wallet to new actor + wallet.ActorID = &actorID + err = db.Save(wallet).Error + if err != nil { + return nil, errors.Wrap(err, "failed to link wallet to new actor") + } + + logger.Infow("created actor and linked to wallet", + "walletID", wallet.ID, + "actorID", actorID, + "address", wallet.Address) + + return &newActor, nil +} + +// loads wallet by actor ID for signing operations +func LoadWalletByActorID(ctx context.Context, db *gorm.DB, actorID string) (*model.Wallet, error) { + db = db.WithContext(ctx) + + var wallet model.Wallet + err := db.Where("actor_id = ?", actorID).First(&wallet).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Errorf("no wallet found for actor %s - actor may not be controlled by this instance", actorID) + } + return nil, errors.Wrap(err, "failed to query wallet by actor ID") + } + + return &wallet, nil +} diff --git a/handler/wallet/sign_test.go b/handler/wallet/sign_test.go new file mode 100644 index 000000000..c7dab4cbe --- /dev/null +++ b/handler/wallet/sign_test.go @@ -0,0 +1,179 @@ +package wallet + +import ( + "context" + "testing" + + "github.com/cockroachdb/errors" + "github.com/data-preservation-programs/singularity/model" + "github.com/data-preservation-programs/singularity/util/testutil" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/ybbus/jsonrpc/v3" + "gorm.io/gorm" +) + +type MockRPCClient struct { + mock.Mock +} + +func (m *MockRPCClient) Call(ctx context.Context, method string, params ...any) (*jsonrpc.RPCResponse, error) { + panic("implement me") +} + +func (m *MockRPCClient) CallRaw(ctx context.Context, request *jsonrpc.RPCRequest) (*jsonrpc.RPCResponse, error) { + panic("implement me") +} + +func (m *MockRPCClient) CallFor(ctx context.Context, out any, method string, params ...any) error { + return m.Called(ctx, out, method, params).Error(0) +} + +func (m *MockRPCClient) CallBatch(ctx context.Context, requests jsonrpc.RPCRequests) (jsonrpc.RPCResponses, error) { + panic("implement me") +} + +func (m *MockRPCClient) CallBatchRaw(ctx context.Context, requests jsonrpc.RPCRequests) (jsonrpc.RPCResponses, error) { + panic("implement me") +} + +func TestGetOrCreateActor_AlreadyLinked(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + actorID := "f01234" + require.NoError(t, db.Create(&model.Actor{ID: actorID, Address: "f3abc"}).Error) + w := model.Wallet{ + Address: "f3abc", KeyPath: "/tmp/key-linked", KeyStore: "local", + ActorID: &actorID, + } + require.NoError(t, db.Create(&w).Error) + + // lotusClient is nil — must not be called + actor, err := GetOrCreateActor(ctx, db, nil, &w) + require.NoError(t, err) + require.Equal(t, actorID, actor.ID) + require.Equal(t, "f3abc", actor.Address) + }) +} + +func TestGetOrCreateActor_LotusLookupFails(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + w := model.Wallet{ + Address: "f3unfunded", KeyPath: "/tmp/key-unfunded", KeyStore: "local", + } + require.NoError(t, db.Create(&w).Error) + + lotus := new(MockRPCClient) + lotus.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), + "Filecoin.StateLookupID", []any{"f3unfunded", nil}). + Return(errors.New("actor not found")) + + _, err := GetOrCreateActor(ctx, db, lotus, &w) + require.Error(t, err) + require.ErrorContains(t, err, "not found on-chain") + require.ErrorContains(t, err, "may need funding") + lotus.AssertExpectations(t) + }) +} + +func TestGetOrCreateActor_CreateNewActor(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + w := model.Wallet{ + Address: "f3new", KeyPath: "/tmp/key-new", KeyStore: "local", + } + require.NoError(t, db.Create(&w).Error) + + lotus := new(MockRPCClient) + lotus.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), + "Filecoin.StateLookupID", []any{"f3new", nil}). + Return(nil).Run(func(args mock.Arguments) { + *args.Get(1).(*string) = "f09999" + }) + + actor, err := GetOrCreateActor(ctx, db, lotus, &w) + require.NoError(t, err) + require.Equal(t, "f09999", actor.ID) + require.Equal(t, "f3new", actor.Address) + + // wallet should now be linked + var updated model.Wallet + require.NoError(t, db.First(&updated, w.ID).Error) + require.NotNil(t, updated.ActorID) + require.Equal(t, "f09999", *updated.ActorID) + + // actor should exist in DB + var dbActor model.Actor + require.NoError(t, db.First(&dbActor, "id = ?", "f09999").Error) + require.Equal(t, "f3new", dbActor.Address) + + lotus.AssertExpectations(t) + }) +} + +func TestGetOrCreateActor_LinkExistingActor(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + // actor exists in DB but wallet is not yet linked to it + require.NoError(t, db.Create(&model.Actor{ID: "f05555", Address: "f3existing"}).Error) + + w := model.Wallet{ + Address: "f3existing", KeyPath: "/tmp/key-existing", KeyStore: "local", + } + require.NoError(t, db.Create(&w).Error) + + lotus := new(MockRPCClient) + lotus.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), + "Filecoin.StateLookupID", []any{"f3existing", nil}). + Return(nil).Run(func(args mock.Arguments) { + *args.Get(1).(*string) = "f05555" + }) + + actor, err := GetOrCreateActor(ctx, db, lotus, &w) + require.NoError(t, err) + require.Equal(t, "f05555", actor.ID) + + // wallet should now be linked + var updated model.Wallet + require.NoError(t, db.First(&updated, w.ID).Error) + require.NotNil(t, updated.ActorID) + require.Equal(t, "f05555", *updated.ActorID) + + lotus.AssertExpectations(t) + }) +} + +func TestGetOrCreateActor_ActorLinkedToDifferentWallet(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + actorID := "f07777" + require.NoError(t, db.Create(&model.Actor{ID: actorID, Address: "f3other"}).Error) + + // first wallet already linked to this actor + other := model.Wallet{ + Address: "f3other", KeyPath: "/tmp/key-other", KeyStore: "local", + ActorID: &actorID, + } + require.NoError(t, db.Create(&other).Error) + + // second wallet tries to claim the same actor + w := model.Wallet{ + Address: "f3conflict", KeyPath: "/tmp/key-conflict", KeyStore: "local", + } + require.NoError(t, db.Create(&w).Error) + + lotus := new(MockRPCClient) + lotus.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), + "Filecoin.StateLookupID", []any{"f3conflict", nil}). + Return(nil).Run(func(args mock.Arguments) { + *args.Get(1).(*string) = actorID + }) + + _, err := GetOrCreateActor(ctx, db, lotus, &w) + require.Error(t, err) + require.ErrorContains(t, err, "already linked to wallet") + + // wallet must remain unlinked + var updated model.Wallet + require.NoError(t, db.First(&updated, w.ID).Error) + require.Nil(t, updated.ActorID) + + lotus.AssertExpectations(t) + }) +} diff --git a/model/migrate.go b/model/migrate.go index 53cfa834c..f1ca258c1 100644 --- a/model/migrate.go +++ b/model/migrate.go @@ -26,6 +26,7 @@ var Tables = []any{ &CarBlock{}, &Deal{}, &Schedule{}, + &Actor{}, &Wallet{}, &PDPProofSet{}, } @@ -220,6 +221,7 @@ var sequenceTables = []string{ "car_blocks", "deals", "schedules", + "wallets", } // fixPostgresSequences detects and fixes out-of-sync sequences. diff --git a/model/preparation.go b/model/preparation.go index d5edc194b..e1937c282 100644 --- a/model/preparation.go +++ b/model/preparation.go @@ -46,7 +46,7 @@ type Preparation struct { NoDag bool `json:"noDag"` // Associations - Wallets []Wallet `gorm:"many2many:wallet_assignments;constraint:OnDelete:CASCADE" json:"wallets,omitempty" swaggerignore:"true" table:"expand"` + Wallets []Wallet `gorm:"many2many:wallet_assignments;constraint:OnDelete:CASCADE" json:"wallets,omitempty" swaggerignore:"true" table:"expand"` SourceStorages []Storage `gorm:"many2many:source_attachments;constraint:OnDelete:CASCADE" json:"sourceStorages,omitempty" table:"expand;header:Source Storages:"` OutputStorages []Storage `gorm:"many2many:output_attachments;constraint:OnDelete:CASCADE" json:"outputStorages,omitempty" table:"expand;header:Output Storages:"` } diff --git a/model/replication.go b/model/replication.go index 6e35e3127..386fddb69 100644 --- a/model/replication.go +++ b/model/replication.go @@ -92,8 +92,8 @@ func StoragePricePerEpochToPricePerDeal(price string, dealSize int64, durationEp type DealID uint64 // Deal is the deal model for all deals made by deal pusher or tracked by the tracker. -// The index on PieceCID is used to track replication of the same piece CID. -// The index on State and ClientID is used to calculate number and size of pending deals. +// index on PieceCID tracks replication of same piece +// index on State and ClientID calculates pending deals type Deal struct { ID DealID `gorm:"primaryKey" json:"id" table:"verbose"` CreatedAt time.Time `json:"createdAt" table:"verbose;format:2006-01-02 15:04:05"` @@ -123,7 +123,7 @@ type Deal struct { ScheduleID *ScheduleID `json:"scheduleId" table:"verbose"` Schedule *Schedule `gorm:"foreignKey:ScheduleID;constraint:OnDelete:SET NULL" json:"schedule,omitempty" swaggerignore:"true" table:"expand"` ClientID string `gorm:"index:idx_pending" json:"clientId"` - Wallet *Wallet `gorm:"foreignKey:ClientID;constraint:OnDelete:SET NULL" json:"wallet,omitempty" swaggerignore:"true" table:"expand"` + Actor *Actor `gorm:"foreignKey:ClientID;constraint:OnDelete:SET NULL" json:"actor,omitempty" swaggerignore:"true" table:"expand"` } // Key returns a mostly unique key to match deal from locally proposed deals and deals from the chain. @@ -167,10 +167,18 @@ type Schedule struct { Preparation *Preparation `gorm:"foreignKey:PreparationID;constraint:OnDelete:CASCADE" json:"preparation,omitempty" swaggerignore:"true" table:"expand"` } -type Wallet struct { - ID string `gorm:"primaryKey;size:15" json:"id"` // ID is the short ID of the wallet - Address string `gorm:"index" json:"address"` // Address is the Filecoin full address of the wallet - PrivateKey string `json:"privateKey,omitempty" table:"-"` // PrivateKey is the private key of the wallet +// on-chain actor identity tracked by singularity +// actor may or may not be controlled by us (linked via optional WalletID) +// TODO: after migration, add WalletID field linking to new Wallet model +type Actor struct { + ID string `gorm:"primaryKey;size:15" json:"id"` // actor ID (f0...) + Address string `gorm:"index" json:"address"` // filecoin address + PrivateKey string `json:"privateKey,omitempty" table:"-"` // TODO: orphaned column, will be dropped by export-keys command +} + +// GORM will rename "wallets" table to "actors" on AutoMigrate +func (Actor) TableName() string { + return "actors" } // PDPProofSet tracks on-chain PDP proof set state derived from contract events. diff --git a/model/wallet.go b/model/wallet.go new file mode 100644 index 000000000..556f16a57 --- /dev/null +++ b/model/wallet.go @@ -0,0 +1,21 @@ +package model + +// private key stored in external keystore, can be linked to on-chain actor +// wallets can exist before actors are created on-chain +type Wallet struct { + ID uint `gorm:"primaryKey" json:"id"` + + KeyPath string `gorm:"uniqueIndex;not null" json:"keyPath"` // absolute path to key file + KeyStore string `gorm:"default:'local';not null" json:"keyStore"` // local, yubikey, aws-kms, etc + Address string `gorm:"index;not null" json:"address"` // filecoin address (f1.../f3...) + Name string `json:"name,omitempty"` // optional label + + ActorID *string `gorm:"index;size:15" json:"actorId,omitempty"` // nullable, links to on-chain actor f0... + + Actor *Actor `gorm:"foreignKey:ActorID;references:ID;constraint:OnDelete:SET NULL" json:"actor,omitempty" swaggerignore:"true" table:"expand"` +} + +// GORM will rename "wallet_keys" table to "wallets" on AutoMigrate +func (Wallet) TableName() string { + return "wallets" +} diff --git a/replication/makedeal.go b/replication/makedeal.go index b074b2d79..a8877e927 100644 --- a/replication/makedeal.go +++ b/replication/makedeal.go @@ -21,11 +21,11 @@ import ( "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/big" "github.com/filecoin-project/go-state-types/builtin/v9/market" + "github.com/filecoin-project/go-state-types/crypto" "github.com/filecoin-shipyard/boostly" "github.com/google/uuid" "github.com/ipfs/go-cid" "github.com/jellydator/ttlcache/v3" - "github.com/jsign/go-filsigner/wallet" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peerstore" @@ -41,6 +41,11 @@ const ( var ErrNoSupportedProtocols = errors.New("no supported protocols") +// ProposalSigner signs deal proposal bytes and returns a filecoin signature. +// Callers construct this from a keystore + wallet before calling MakeDeal, +// keeping the deal maker decoupled from database and key storage. +type ProposalSigner func([]byte) (*crypto.Signature, error) + //nolint:tagliatelle type MinerInfo struct { PeerIDEncoded string `json:"PeerID"` @@ -55,7 +60,7 @@ type DealProviderCollateralBound struct { } type DealMaker interface { - MakeDeal(ctx context.Context, walletObj model.Wallet, car model.Car, dealConfig DealConfig) (*model.Deal, error) + MakeDeal(ctx context.Context, actorObj model.Actor, car model.Car, dealConfig DealConfig, signer ProposalSigner) (*model.Deal, error) } // DealMakerImpl is an implementation of a deal-making component for a Filecoin-like network. @@ -491,7 +496,7 @@ func (d DealConfig) GetPrice(pieceSize int64, duration time.Duration) big.Int { // // Parameters: // - ctx context.Context: The context to use for timeouts and cancellation. -// - walletObj model.Wallet: The client's wallet, containing the client's addresses and private key. +// - actorObj model.Actor: The on-chain actor identity for deal signing. // - car model.Car: The car file that contains the data to be stored. // - dealConfig DealConfig: The configuration for the deal, including price and duration. // @@ -515,14 +520,14 @@ func (d DealConfig) GetPrice(pieceSize int64, duration time.Duration) big.Int { // - Deal proposal rejected by the provider. // // - No supported protocol found between client and provider. -func (d DealMakerImpl) MakeDeal(ctx context.Context, walletObj model.Wallet, - car model.Car, dealConfig DealConfig, +func (d DealMakerImpl) MakeDeal(ctx context.Context, actorObj model.Actor, + car model.Car, dealConfig DealConfig, signer ProposalSigner, ) (*model.Deal, error) { - logger.Infow("making deal", "client", walletObj.ID, "pieceCID", car.PieceCID.String(), "provider", dealConfig.Provider) + logger.Infow("making deal", "client", actorObj.ID, "pieceCID", car.PieceCID.String(), "provider", dealConfig.Provider) now := time.Now().UTC() - addr, err := address.NewFromString(walletObj.Address) + addr, err := address.NewFromString(actorObj.Address) if err != nil { - return nil, errors.Wrapf(err, "failed to parse wallet address %s", walletObj.Address) + return nil, errors.Wrapf(err, "failed to parse wallet address %s", actorObj.Address) } pvd, err := address.NewFromString(dealConfig.Provider) @@ -578,7 +583,7 @@ func (d DealMakerImpl) MakeDeal(ctx context.Context, walletObj model.Wallet, return nil, errors.Wrapf(err, "failed to serialize deal proposal %s", proposal) } - signature, err := wallet.WalletSign(walletObj.PrivateKey, proposalBytes) + signature, err := signer(proposalBytes) if err != nil { return nil, errors.Wrap(err, "failed to sign deal proposal") } @@ -590,7 +595,7 @@ func (d DealMakerImpl) MakeDeal(ctx context.Context, walletObj model.Wallet, dealModel := &model.Deal{ State: model.DealProposed, - ClientID: walletObj.ID, + ClientID: actorObj.ID, Provider: dealConfig.Provider, Label: cid.Cid(car.RootCID).String(), PieceCID: car.PieceCID, diff --git a/replication/makedeal_test.go b/replication/makedeal_test.go index b1496c2fd..471c885e8 100644 --- a/replication/makedeal_test.go +++ b/replication/makedeal_test.go @@ -6,8 +6,10 @@ import ( "testing" "time" + "github.com/data-preservation-programs/go-synapse/signer" "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/testutil" "github.com/filecoin-project/go-address" cborutil "github.com/filecoin-project/go-cbor-util" "github.com/filecoin-project/go-fil-markets/storagemarket/network" @@ -105,8 +107,6 @@ func setupBasicHost(t *testing.T, ctx context.Context, port string) host.Host { } func TestDealMaker_MakeDeal(t *testing.T) { - addr := "f1fib3pv7jua2ockdugtz7viz3cyy6lkhh7rfx3sa" - key := "7b2254797065223a22736563703235366b31222c22507269766174654b6579223a226b35507976337148327349586343595a58594f5775453149326e32554539436861556b6c4e36695a5763453d227d" ctx, cancel := context.WithCancel(context.Background()) defer cancel() server := setupBasicHost(t, ctx, "10001") @@ -115,10 +115,17 @@ func TestDealMaker_MakeDeal(t *testing.T) { defer client.Close() maker := NewDealMaker(nil, client, time.Hour, time.Second) defer maker.Close() - wallet := model.Wallet{ - ID: "f047684", - Address: addr, - PrivateKey: key, + + // build a real signer from the test key + s, err := signer.FromLotusExport(testutil.TestPrivateKeyHex) + require.NoError(t, err) + proposalSigner := ProposalSigner(func(msg []byte) (*crypto.Signature, error) { + return s.Sign(msg) + }) + + actor := model.Actor{ + ID: "f047684", + Address: testutil.TestWalletAddr, } rootCID, err := cid.Decode("bafy2bzaceczlclcg4notjmrz4ayenf7fi4mngnqbgjs27r3resyhzwxjnviay") require.NoError(t, err) @@ -154,14 +161,14 @@ func TestDealMaker_MakeDeal(t *testing.T) { StorageProposalV120, }, ttlcache.DefaultTTL) - _, err = maker.MakeDeal(ctx, wallet, car, dealConfig) + _, err = maker.MakeDeal(ctx, actor, car, dealConfig, proposalSigner) require.NoError(t, err) maker.protocolsCache.Set(server.ID(), []protocol.ID{ StorageProposalV111, }, ttlcache.DefaultTTL) - _, err = maker.MakeDeal(ctx, wallet, car, dealConfig) + _, err = maker.MakeDeal(ctx, actor, car, dealConfig, proposalSigner) require.NoError(t, err) } diff --git a/replication/wallet.go b/replication/wallet.go index 74e457c66..3a0aed0f1 100644 --- a/replication/wallet.go +++ b/replication/wallet.go @@ -28,27 +28,7 @@ var ErrNoWallet = errors.New("no wallets to choose from") var ErrNoDatacap = errors.New("no wallets have enough datacap") -// Choose selects a random Wallet from the provided slice of Wallets. -// -// The Choose function of the RandomWalletChooser type randomly selects -// a Wallet from a given slice of Wallets. If the slice is empty, the function -// returns an error. It uses a cryptographically secure random number generator -// to make the selection. -// -// Parameters: -// - ctx context.Context: The context to use for cancellation and deadlines, -// although it is not used in this implementation. -// - wallets []model.Wallet: A slice of Wallet objects from which a random Wallet -// will be chosen. -// -// Returns: -// - model.Wallet: The randomly chosen Wallet object from the provided slice. -// - error: An error that will be returned if any issues were encountered while trying -// to choose a Wallet. This includes the case when the input slice is empty, -// in which case ErrNoWallet will be returned, or if there is an issue generating -// a random number. -func (w RandomWalletChooser) Choose(ctx context.Context, wallets []model.Wallet) (model.Wallet, error) { - // Check if the wallets slice is empty +func (w RandomWalletChooser) Choose(_ context.Context, wallets []model.Wallet) (model.Wallet, error) { if len(wallets) == 0 { return model.Wallet{}, ErrNoWallet } @@ -57,10 +37,11 @@ func (w RandomWalletChooser) Choose(ctx context.Context, wallets []model.Wallet) if err != nil { return model.Wallet{}, errors.WithStack(err) } - chosenWallet := wallets[randomPick.Int64()] - return chosenWallet, nil + return wallets[randomPick.Int64()], nil } +// DatacapWalletChooser selects a wallet whose linked actor has sufficient datacap. +// only meaningful for market deals where datacap matters. type DatacapWalletChooser struct { db *gorm.DB cache *ttlcache.Cache[string, int64] @@ -69,7 +50,7 @@ type DatacapWalletChooser struct { } func NewDatacapWalletChooser(db *gorm.DB, cacheTTL time.Duration, - lotusAPI string, lotusToken string, min uint64, //nolint:predeclared // We're ok with using the same name as the predeclared identifier here + lotusAPI string, lotusToken string, min uint64, //nolint:predeclared ) DatacapWalletChooser { cache := ttlcache.New[string, int64]( ttlcache.WithTTL[string, int64](cacheTTL), @@ -85,6 +66,9 @@ func NewDatacapWalletChooser(db *gorm.DB, cacheTTL time.Duration, } func (w DatacapWalletChooser) getDatacap(ctx context.Context, wallet model.Wallet) (int64, error) { + if wallet.ActorID == nil { + return 0, errors.Newf("wallet %s has no linked actor", wallet.Address) + } var result string err := w.lotusClient.CallFor(ctx, &result, "Filecoin.StateMarketBalance", wallet.Address, nil) if err != nil { @@ -111,10 +95,13 @@ func (w DatacapWalletChooser) getDatacapCached(ctx context.Context, wallet model } func (w DatacapWalletChooser) getPendingDeals(ctx context.Context, wallet model.Wallet) (int64, error) { + if wallet.ActorID == nil { + return 0, nil + } var totalPieceSize int64 err := w.db.WithContext(ctx).Model(&model.Deal{}). Select("COALESCE(SUM(piece_size), 0)"). - Where("client_id = ? AND verified AND state = ?", wallet.ID, model.DealProposed). + Where("client_id = ? AND verified AND state = ?", *wallet.ActorID, model.DealProposed). Scan(&totalPieceSize). Error if err != nil { @@ -124,59 +111,35 @@ func (w DatacapWalletChooser) getPendingDeals(ctx context.Context, wallet model. return totalPieceSize, nil } -// Choose selects a random Wallet from the provided slice of Wallets based on certain criteria. -// -// The Choose function of the DatacapWalletChooser type filters the given slice of Wallets -// based on a specific criterion, which is whether the datacap for the wallet minus -// the pending deals for the wallet is greater or equal to a minimum threshold (w.min). -// From the filtered eligible Wallets, the function then randomly selects one Wallet. -// It uses a cryptographically secure random number generator to make the selection. -// If the initial slice of Wallets is empty, or if no Wallets meet the criteria, -// the function returns an error. -// -// Parameters: -// - ctx context.Context: The context to use for cancellation and deadlines, used -// in the datacap and pending deals fetching operations. -// - wallets []model.Wallet: A slice of Wallet objects from which a random Wallet -// will be chosen based on the criteria. -// -// Returns: -// - model.Wallet: The randomly chosen Wallet object from the filtered eligible Wallets. -// - error: An error that will be returned if any issues were encountered while trying -// to choose a Wallet. This includes the case when the input slice is empty, -// in which case ErrNoWallet will be returned, when no Wallets meet the criteria, -// in which case ErrNoDatacap will be returned, or if there is an issue generating -// a random number. func (w DatacapWalletChooser) Choose(ctx context.Context, wallets []model.Wallet) (model.Wallet, error) { if len(wallets) == 0 { return model.Wallet{}, ErrNoWallet } - var eligibleWallets []model.Wallet + var eligible []model.Wallet for _, wallet := range wallets { datacap, err := w.getDatacapCached(ctx, wallet) if err != nil { - logger.Errorw("failed to get datacap for wallet", "wallet", wallet.Address, "error", err) + logger.Errorw("failed to get datacap for wallet", "address", wallet.Address, "error", err) continue } pendingDeals, err := w.getPendingDeals(ctx, wallet) if err != nil { - logger.Errorw("failed to get pending deals for wallet", "wallet", wallet.Address, "error", err) + logger.Errorw("failed to get pending deals for wallet", "address", wallet.Address, "error", err) continue } if datacap-pendingDeals >= int64(w.min) { - eligibleWallets = append(eligibleWallets, wallet) + eligible = append(eligible, wallet) } } - if len(eligibleWallets) == 0 { + if len(eligible) == 0 { return model.Wallet{}, ErrNoDatacap } - randomPick, err := rand.Int(rand.Reader, big.NewInt(int64(len(eligibleWallets)))) + randomPick, err := rand.Int(rand.Reader, big.NewInt(int64(len(eligible)))) if err != nil { return model.Wallet{}, errors.WithStack(err) } - chosenWallet := eligibleWallets[randomPick.Int64()] - return chosenWallet, nil + return eligible[randomPick.Int64()], nil } diff --git a/replication/wallet_test.go b/replication/wallet_test.go index cc41d3d9c..6fd5d820f 100644 --- a/replication/wallet_test.go +++ b/replication/wallet_test.go @@ -8,6 +8,7 @@ import ( "github.com/cockroachdb/errors" "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/util/testutil" + "github.com/gotidy/ptr" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/ybbus/jsonrpc/v3" @@ -46,31 +47,38 @@ func TestDatacapWalletChooser_Choose(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { lotusClient := new(MockRPCClient) - // Set up the test data - wallets := []model.Wallet{ - {ID: "1", Address: "address1"}, - {ID: "2", Address: "address2"}, - {ID: "3", Address: "address3"}, - {ID: "4", Address: "address4"}, + ids := []string{"a", "b", "c", "d"} + actors := make([]model.Actor, len(ids)) + for i, id := range ids { + actors[i] = model.Actor{ID: "actor" + id, Address: "address" + id} + } + require.NoError(t, db.Create(&actors).Error) + + wallets := make([]model.Wallet, len(ids)) + for i, id := range ids { + wallets[i] = model.Wallet{ + Address: "address" + id, KeyPath: "/tmp/key-" + id, + KeyStore: "local", ActorID: ptr.String("actor" + id), + } + require.NoError(t, db.Create(&wallets[i]).Error) } - // Set up expectations for the lotusClient mock - lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"address1", nil}). + lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"addressa", nil}). Return(nil).Run(func(args mock.Arguments) { resultPtr := args.Get(1).(*string) *resultPtr = "1000000" }) - lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"address2", nil}). + lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"addressb", nil}). Return(errors.New("failed to get datacap")) - lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"address3", nil}). + lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"addressc", nil}). Return(nil).Run(func(args mock.Arguments) { resultPtr := args.Get(1).(*string) *resultPtr = "1000000" }) - lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"address4", nil}). + lotusClient.On("CallFor", mock.Anything, mock.AnythingOfType("*string"), "Filecoin.StateMarketBalance", []any{"addressd", nil}). Return(nil).Run(func(args mock.Arguments) { resultPtr := args.Get(1).(*string) *resultPtr = "900000" @@ -79,15 +87,12 @@ func TestDatacapWalletChooser_Choose(t *testing.T) { chooser := NewDatacapWalletChooser(db, time.Minute, "lotusAPI", "lotusToken", 900001) chooser.lotusClient = lotusClient - err := db.Create(&wallets).Error - require.NoError(t, err) - err = db.Create(&model.Deal{ - ClientID: "3", + require.NoError(t, db.Create(&model.Deal{ + ClientID: "actorc", Verified: true, State: model.DealProposed, PieceSize: 500000, - }).Error - require.NoError(t, err) + }).Error) t.Run("Choose wallet with empty wallet", func(t *testing.T) { _, err := chooser.Choose(context.Background(), []model.Wallet{}) @@ -97,7 +102,7 @@ func TestDatacapWalletChooser_Choose(t *testing.T) { t.Run("Choose wallet with sufficient datacap", func(t *testing.T) { chosenWallet, err := chooser.Choose(context.Background(), []model.Wallet{wallets[0], wallets[1]}) require.NoError(t, err) - require.Equal(t, "address1", chosenWallet.Address) + require.Equal(t, "addressa", chosenWallet.Address) }) t.Run("Choose wallet with insufficient datacap", func(t *testing.T) { @@ -111,8 +116,8 @@ func TestRandomWalletChooser(t *testing.T) { chooser := &RandomWalletChooser{} ctx := context.Background() wallet, err := chooser.Choose(ctx, []model.Wallet{ - {ID: "1", Address: "address1"}, - {ID: "2", Address: "address2"}, + {Address: "address1"}, + {Address: "address2"}, }) require.NoError(t, err) require.Contains(t, wallet.Address, "address") diff --git a/service/dealpusher/dealpusher.go b/service/dealpusher/dealpusher.go index 2e8321afc..0009ff0dc 100644 --- a/service/dealpusher/dealpusher.go +++ b/service/dealpusher/dealpusher.go @@ -10,16 +10,21 @@ import ( "github.com/cockroachdb/errors" "github.com/data-preservation-programs/singularity/analytics" "github.com/data-preservation-programs/singularity/database" + "github.com/data-preservation-programs/singularity/handler/wallet" "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/replication" "github.com/data-preservation-programs/singularity/service/healthcheck" "github.com/data-preservation-programs/singularity/util" + "github.com/data-preservation-programs/singularity/util/keystore" + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/crypto" "github.com/google/uuid" "github.com/ipfs/go-cid" "github.com/ipfs/go-log/v2" "github.com/libp2p/go-libp2p/core/host" "github.com/rjNemo/underscore" "github.com/robfig/cron/v3" + "github.com/ybbus/jsonrpc/v3" "gorm.io/gorm" ) @@ -35,11 +40,14 @@ var waitPendingInterval = time.Minute // DealPusher represents a struct that encapsulates the data and functionality related to pushing deals in a replication process. type DealPusher struct { - dbNoContext *gorm.DB // Pointer to a gorm.DB object representing a database connection. - walletChooser replication.WalletChooser // Object responsible for choosing a wallet for replication. - dealMaker replication.DealMaker // Object responsible for making a deal in replication. - pdpProofSetManager PDPProofSetManager // Optional PDP proof set lifecycle manager. - pdpTxConfirmer PDPTransactionConfirmer // Optional PDP transaction confirmer. + dbNoContext *gorm.DB // Pointer to a gorm.DB object representing a database connection. + keyStore keystore.KeyStore // Keystore for loading private keys + lotusClient jsonrpc.RPCClient // Lotus JSON-RPC client for chain queries + walletChooser replication.WalletChooser // Object responsible for choosing a wallet for replication. + dealMaker replication.DealMaker // Object responsible for making a deal in replication. + pdpProofSetManager PDPProofSetManager // Optional PDP proof set lifecycle manager. + pdpTxConfirmer PDPTransactionConfirmer // Optional PDP transaction confirmer. + pdpSchedulingConfig PDPSchedulingConfig // PDP scheduling config for root batching and tx confirmation. // Resolver is injected so tests and future wiring can switch deal type behavior without coupling DealPusher to config storage. scheduleDealTypeResolver func(schedule *model.Schedule) model.DealType workerID uuid.UUID // UUID identifying the associated worker. @@ -365,10 +373,21 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) return model.ScheduleError, errors.Wrap(err, "failed to choose wallet") } + // market deals need the on-chain actor for the deal proposal; + // lazily resolve if not yet linked (first use after offline import) + actorObj, err := wallet.GetOrCreateActor(ctx, db, d.lotusClient, &walletObj) + if err != nil { + return model.ScheduleError, errors.Wrapf(err, "failed to resolve actor for wallet %s", walletObj.Address) + } + + proposalSigner := replication.ProposalSigner(func(msg []byte) (*crypto.Signature, error) { + return wallet.SignWithWallet(d.keyStore, walletObj, msg) + }) + err = retry.Do(func() error { dealModel, err = d.dealMaker.MakeDeal( ctx, - walletObj, + *actorObj, car, replication.DealConfig{ Provider: schedule.Provider, @@ -382,7 +401,8 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) PricePerDeal: schedule.PricePerDeal, PricePerGB: schedule.PricePerGB, PricePerGBEpoch: schedule.PricePerGBEpoch, - }) + }, + proposalSigner) if err != nil { Logger.Errorw("failed to send deal", "error", err, "provider", schedule.Provider) if strings.Contains(err.Error(), "deal proposal is identical") { @@ -433,11 +453,37 @@ func (d *DealPusher) runSchedule(ctx context.Context, schedule *model.Schedule) func (d *DealPusher) resolveScheduleDealType(schedule *model.Schedule) model.DealType { if d.scheduleDealTypeResolver == nil { - return model.DealTypeMarket + return inferScheduleDealType(schedule) } return d.scheduleDealTypeResolver(schedule) } +func defaultPDPSchedulingConfig() PDPSchedulingConfig { + return PDPSchedulingConfig{ + BatchSize: 128, + GasLimit: 5_000_000, + ConfirmationDepth: 5, + PollingInterval: 30 * time.Second, + } +} + +// inferScheduleDealType uses the provider address protocol as the discriminator: +// delegated (f4) addresses are FEVM contracts that speak PDP, everything else +// is a traditional miner actor that speaks market deals. +func inferScheduleDealType(schedule *model.Schedule) model.DealType { + if schedule == nil { + return model.DealTypeMarket + } + providerAddr, err := address.NewFromString(schedule.Provider) + if err != nil { + return model.DealTypeMarket + } + if providerAddr.Protocol() == address.Delegated { + return model.DealTypePDP + } + return model.DealTypeMarket +} + func (d *DealPusher) runPDPSchedule(_ context.Context, _ *model.Schedule) (model.ScheduleState, error) { if d.pdpProofSetManager == nil || d.pdpTxConfirmer == nil { return model.ScheduleError, errors.New("pdp scheduling dependencies are not configured") @@ -446,7 +492,7 @@ func (d *DealPusher) runPDPSchedule(_ context.Context, _ *model.Schedule) (model } func NewDealPusher(db *gorm.DB, lotusURL string, - lotusToken string, numAttempts uint, maxReplicas uint, + lotusToken string, numAttempts uint, maxReplicas uint, opts ...Option, ) (*DealPusher, error) { if numAttempts <= 1 { numAttempts = 1 @@ -455,25 +501,36 @@ func NewDealPusher(db *gorm.DB, lotusURL string, if err != nil { return nil, errors.Wrap(err, "failed to init host") } - lotusClient := util.NewLotusClient(lotusURL, lotusToken) - dealMaker := replication.NewDealMaker(lotusClient, h, time.Hour, time.Minute) + + ks, err := keystore.NewLocalKeyStore(wallet.GetKeystoreDir()) if err != nil { - return nil, errors.Wrap(err, "failed to init deal maker") + return nil, errors.Wrap(err, "failed to init keystore") } - return &DealPusher{ + + lotusClient := util.NewLotusClient(lotusURL, lotusToken) + dealMaker := replication.NewDealMaker(lotusClient, h, time.Hour, time.Minute) + + dp := &DealPusher{ dbNoContext: db, + keyStore: ks, + lotusClient: lotusClient, activeScheduleCancelFunc: make(map[model.ScheduleID]context.CancelFunc), activeSchedule: make(map[model.ScheduleID]*model.Schedule), cronEntries: make(map[model.ScheduleID]cron.EntryID), walletChooser: &replication.RandomWalletChooser{}, dealMaker: dealMaker, + pdpSchedulingConfig: defaultPDPSchedulingConfig(), workerID: uuid.New(), cron: cron.New(cron.WithLogger(&cronLogger{}), cron.WithLocation(time.UTC), cron.WithParser(cron.NewParser(cron.SecondOptional|cron.Minute|cron.Hour|cron.Dom|cron.Month|cron.Dow|cron.Descriptor))), sendDealAttempts: numAttempts, host: h, maxReplicas: maxReplicas, - }, nil + } + for _, opt := range opts { + opt(dp) + } + return dp, nil } // runOnce is a method of the DealPusher type that runs a single iteration of the deal pushing logic. diff --git a/service/dealpusher/dealpusher_test.go b/service/dealpusher/dealpusher_test.go index 842025352..3c92bec8d 100644 --- a/service/dealpusher/dealpusher_test.go +++ b/service/dealpusher/dealpusher_test.go @@ -28,12 +28,19 @@ func init() { analytics.Enabled = false } +// creates actor record that wallets reference via ActorID FK. +// must be called before schedule creation since wallet.ActorID references actor. +func createTestActor(t *testing.T, db *gorm.DB, actorID string) { + t.Helper() + require.NoError(t, db.Create(&model.Actor{ID: actorID, Address: "f0xx"}).Error) +} + type MockDealMaker struct { mock.Mock } -func (m *MockDealMaker) MakeDeal(ctx context.Context, walletObj model.Wallet, car model.Car, dealConfig replication.DealConfig) (*model.Deal, error) { - args := m.Called(ctx, walletObj, car, dealConfig) +func (m *MockDealMaker) MakeDeal(ctx context.Context, actorObj model.Actor, car model.Car, dealConfig replication.DealConfig, signer replication.ProposalSigner) (*model.Deal, error) { + args := m.Called(ctx, actorObj, car, dealConfig) if args.Get(0) == nil { return nil, args.Error(1) } @@ -41,7 +48,7 @@ func (m *MockDealMaker) MakeDeal(ctx context.Context, walletObj model.Wallet, ca deal.ID = 0 deal.PieceCID = car.PieceCID deal.PieceSize = car.PieceSize - deal.ClientID = walletObj.ID + deal.ClientID = actorObj.ID deal.Provider = dealConfig.Provider deal.Verified = dealConfig.Verified deal.ProposalID = uuid.NewString() @@ -112,7 +119,8 @@ func TestDealMakerService_FailtoSend(t *testing.T) { SourceStorages: []model.Storage{{}}, Wallets: []model.Wallet{ { - ID: client, Address: "f0xx", + Address: "f0xx", KeyPath: "/tmp/fake-key", KeyStore: "local", + ActorID: &client, }, }}, State: model.ScheduleActive, @@ -121,6 +129,7 @@ func TestDealMakerService_FailtoSend(t *testing.T) { MaxPendingDealSize: 2048, TotalDealNumber: 4, } + createTestActor(t, db, client) err = db.Create(&schedule).Error require.NoError(t, err) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("send deal error")) @@ -168,7 +177,8 @@ func TestDealMakerService_Cron(t *testing.T) { SourceStorages: []model.Storage{{}}, Wallets: []model.Wallet{ { - ID: client, Address: "f0xx", + Address: "f0xx", KeyPath: "/tmp/fake-key", KeyStore: "local", + ActorID: &client, }, }}, State: model.ScheduleActive, @@ -176,6 +186,7 @@ func TestDealMakerService_Cron(t *testing.T) { ScheduleDealSize: 1, Provider: provider, } + createTestActor(t, db, client) err = db.Create(&schedule).Error require.NoError(t, err) @@ -263,7 +274,8 @@ func TestDealMakerService_ScheduleWithConstraints(t *testing.T) { SourceStorages: []model.Storage{{}}, Wallets: []model.Wallet{ { - ID: client, Address: "f0xx", + Address: "f0xx", KeyPath: "/tmp/fake-key", KeyStore: "local", + ActorID: &client, }, }}, State: model.ScheduleActive, @@ -272,6 +284,7 @@ func TestDealMakerService_ScheduleWithConstraints(t *testing.T) { MaxPendingDealSize: 2048, TotalDealNumber: 4, } + createTestActor(t, db, client) err = db.Create(&schedule).Error require.NoError(t, err) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ @@ -372,7 +385,8 @@ func TestDealmakerService_Force(t *testing.T) { Preparation: &model.Preparation{ Wallets: []model.Wallet{ { - ID: client, Address: "f0xx", + Address: "f0xx", KeyPath: "/tmp/fake-key", KeyStore: "local", + ActorID: &client, }, }, SourceStorages: []model.Storage{{}}, @@ -381,6 +395,7 @@ func TestDealmakerService_Force(t *testing.T) { Provider: provider, Force: true, } + createTestActor(t, db, client) err = db.Create(&schedule).Error require.NoError(t, err) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ @@ -431,7 +446,8 @@ func TestDealMakerService_MaxReplica(t *testing.T) { Preparation: &model.Preparation{ Wallets: []model.Wallet{ { - ID: client, Address: "f0xx", + Address: "f0xx", KeyPath: "/tmp/fake-key", KeyStore: "local", + ActorID: &client, }, }, SourceStorages: []model.Storage{{}}, @@ -439,6 +455,7 @@ func TestDealMakerService_MaxReplica(t *testing.T) { State: model.ScheduleActive, Provider: provider, } + createTestActor(t, db, client) err = db.Create(&schedule).Error require.NoError(t, err) mockDealmaker.On("MakeDeal", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&model.Deal{ @@ -497,7 +514,8 @@ func TestDealMakerService_NewScheduleOneOff(t *testing.T) { Preparation: &model.Preparation{ Wallets: []model.Wallet{ { - ID: client, Address: "f0xx", + Address: "f0xx", KeyPath: "/tmp/fake-key", KeyStore: "local", + ActorID: &client, }, }, SourceStorages: []model.Storage{{}}, @@ -506,6 +524,7 @@ func TestDealMakerService_NewScheduleOneOff(t *testing.T) { Provider: provider, AllowedPieceCIDs: underscore.Map(pieceCIDs[:5], func(cid model.CID) string { return cid.String() }), } + createTestActor(t, db, client) err = db.Create(&schedule).Error require.NoError(t, err) diff --git a/service/dealpusher/options.go b/service/dealpusher/options.go new file mode 100644 index 000000000..4b9861530 --- /dev/null +++ b/service/dealpusher/options.go @@ -0,0 +1,30 @@ +package dealpusher + +import "github.com/data-preservation-programs/singularity/model" + +// Option customizes DealPusher initialization. +type Option func(*DealPusher) + +func WithPDPProofSetManager(manager PDPProofSetManager) Option { + return func(d *DealPusher) { + d.pdpProofSetManager = manager + } +} + +func WithPDPTransactionConfirmer(confirmer PDPTransactionConfirmer) Option { + return func(d *DealPusher) { + d.pdpTxConfirmer = confirmer + } +} + +func WithPDPSchedulingConfig(cfg PDPSchedulingConfig) Option { + return func(d *DealPusher) { + d.pdpSchedulingConfig = cfg + } +} + +func WithScheduleDealTypeResolver(resolver func(schedule *model.Schedule) model.DealType) Option { + return func(d *DealPusher) { + d.scheduleDealTypeResolver = resolver + } +} diff --git a/service/dealpusher/pdp_api.go b/service/dealpusher/pdp_api.go index 2634213b0..73efc3e55 100644 --- a/service/dealpusher/pdp_api.go +++ b/service/dealpusher/pdp_api.go @@ -6,7 +6,7 @@ import ( "math/big" "time" - "github.com/data-preservation-programs/singularity/model" + "github.com/data-preservation-programs/go-synapse/signer" "github.com/ipfs/go-cid" ) @@ -36,11 +36,13 @@ func (c PDPSchedulingConfig) Validate() error { } // PDPProofSetManager defines proof set lifecycle operations needed by scheduling. +// Both methods take an EVMSigner because they submit FEVM transactions. type PDPProofSetManager interface { // EnsureProofSet returns an existing proof set ID or creates one for this client/provider pair. - EnsureProofSet(ctx context.Context, wallet model.Wallet, provider string) (uint64, error) + // The signer's EVMAddress identifies the client on-chain. + EnsureProofSet(ctx context.Context, evmSigner signer.EVMSigner, provider string) (uint64, error) // QueueAddRoots submits root additions for a proof set and returns the queued tx reference. - QueueAddRoots(ctx context.Context, proofSetID uint64, pieceCIDs []cid.Cid, cfg PDPSchedulingConfig) (*PDPQueuedTx, error) + QueueAddRoots(ctx context.Context, evmSigner signer.EVMSigner, proofSetID uint64, pieceCIDs []cid.Cid, cfg PDPSchedulingConfig) (*PDPQueuedTx, error) } // PDPTransactionConfirmer defines confirmation checks for queued on-chain transactions. diff --git a/service/dealpusher/pdp_wiring_test.go b/service/dealpusher/pdp_wiring_test.go index 5a7dbaa54..2001d1bfa 100644 --- a/service/dealpusher/pdp_wiring_test.go +++ b/service/dealpusher/pdp_wiring_test.go @@ -5,18 +5,20 @@ import ( "testing" "time" + "github.com/data-preservation-programs/go-synapse/signer" "github.com/data-preservation-programs/singularity/model" + "github.com/filecoin-project/go-address" "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" ) type noopPDPProofSetManager struct{} -func (noopPDPProofSetManager) EnsureProofSet(_ context.Context, _ model.Wallet, _ string) (uint64, error) { +func (noopPDPProofSetManager) EnsureProofSet(_ context.Context, _ signer.EVMSigner, _ string) (uint64, error) { return 1, nil } -func (noopPDPProofSetManager) QueueAddRoots(_ context.Context, _ uint64, _ []cid.Cid, _ PDPSchedulingConfig) (*PDPQueuedTx, error) { +func (noopPDPProofSetManager) QueueAddRoots(_ context.Context, _ signer.EVMSigner, _ uint64, _ []cid.Cid, _ PDPSchedulingConfig) (*PDPQueuedTx, error) { return &PDPQueuedTx{Hash: "0x1"}, nil } @@ -31,6 +33,15 @@ func TestDealPusher_ResolveScheduleDealType_DefaultsToMarket(t *testing.T) { require.Equal(t, model.DealTypeMarket, d.resolveScheduleDealType(&model.Schedule{})) } +func TestDealPusher_ResolveScheduleDealType_DelegatedProviderInfersPDP(t *testing.T) { + subaddr := make([]byte, 20) + subaddr[19] = 1 + providerAddr, err := address.NewDelegatedAddress(10, subaddr) + require.NoError(t, err) + d := &DealPusher{} + require.Equal(t, model.DealTypePDP, d.resolveScheduleDealType(&model.Schedule{Provider: providerAddr.String()})) +} + func TestDealPusher_RunSchedule_PDPWithoutDependenciesReturnsConfiguredError(t *testing.T) { d := &DealPusher{ scheduleDealTypeResolver: func(_ *model.Schedule) model.DealType { diff --git a/service/dealtracker/dealtracker.go b/service/dealtracker/dealtracker.go index c878d27f1..26b6f2cee 100644 --- a/service/dealtracker/dealtracker.go +++ b/service/dealtracker/dealtracker.go @@ -424,16 +424,16 @@ func (d *DealTracker) runOnce(ctx context.Context) error { var lastEpoch int32 db := d.dbNoContext.WithContext(ctx) - var wallets []model.Wallet - err = db.Find(&wallets).Error + var actors []model.Actor + err = db.Find(&actors).Error if err != nil { - return errors.Wrap(err, "failed to get wallets from database") + return errors.Wrap(err, "failed to get actors from database") } - walletIDs := make(map[string]struct{}) - for _, wallet := range wallets { - Logger.Infof("tracking deals for wallet %s", wallet.ID) - walletIDs[wallet.ID] = struct{}{} + actorIDs := make(map[string]struct{}) + for _, actor := range actors { + Logger.Infof("tracking deals for actor %s", actor.ID) + actorIDs[actor.ID] = struct{}{} } knownDeals := make(map[uint64]model.DealState) @@ -488,7 +488,7 @@ func (d *DealTracker) runOnce(ctx context.Context) error { if deal.State.LastUpdatedEpoch > lastEpoch { lastEpoch = deal.State.LastUpdatedEpoch } - _, ok := walletIDs[deal.Proposal.Client] + _, ok := actorIDs[deal.Proposal.Client] if !ok { return nil } @@ -556,7 +556,7 @@ func (d *DealTracker) runOnce(ctx context.Context) error { return db.Create(&model.Deal{ DealID: &dealID, State: newState, - DealType: model.DealTypeMarket, // Legacy market deal (f05) + DealType: model.DealTypeMarket, ClientID: deal.Proposal.Client, Provider: deal.Proposal.Provider, Label: deal.Proposal.Label, diff --git a/service/dealtracker/dealtracker_test.go b/service/dealtracker/dealtracker_test.go index fd6888103..a896d4252 100644 --- a/service/dealtracker/dealtracker_test.go +++ b/service/dealtracker/dealtracker_test.go @@ -152,7 +152,7 @@ func TestTrackDeal(t *testing.T) { func TestRunOnce(t *testing.T) { testutil.SkipIfNotExternalAPI(t) testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { - err := db.Create(&model.Wallet{ + err := db.Create(&model.Actor{ ID: "t0100", Address: "t3xxx", }).Error diff --git a/service/pdptracker/eventprocessor.go b/service/pdptracker/eventprocessor.go index 4d11a2e89..aafe9ea86 100644 --- a/service/pdptracker/eventprocessor.go +++ b/service/pdptracker/eventprocessor.go @@ -207,8 +207,8 @@ func reconcileProofSetPieces(ctx context.Context, db *gorm.DB, rpcClient *ChainP return nil } - var wallet model.Wallet - if err := db.Where("address = ?", ps.ClientAddress).First(&wallet).Error; err != nil { + var actor model.Actor + if err := db.Where("address = ?", ps.ClientAddress).First(&actor).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { Logger.Debugw("pieces changed for untracked client", "setId", setID, "client", ps.ClientAddress) return nil @@ -255,7 +255,7 @@ func reconcileProofSetPieces(ctx context.Context, db *gorm.DB, rpcClient *ChainP return db.Create(&model.Deal{ DealType: model.DealTypePDP, State: initialState, - ClientID: wallet.ID, + ClientID: actor.ID, Provider: ps.Provider, PieceCID: modelCID, ProofSetID: ptr.Of(setID), diff --git a/service/pdptracker/eventprocessor_test.go b/service/pdptracker/eventprocessor_test.go index 49ae328a3..e77d869d0 100644 --- a/service/pdptracker/eventprocessor_test.go +++ b/service/pdptracker/eventprocessor_test.go @@ -82,7 +82,7 @@ func pgTest(t *testing.T, fn func(t *testing.T, e pgTestEnv)) { func (e pgTestEnv) setupFixtures(t *testing.T) { t.Helper() - require.NoError(t, e.db.Create(&model.Wallet{ + require.NoError(t, e.db.Create(&model.Actor{ ID: "f0100", Address: e.listenerFil.String(), }).Error) require.NoError(t, e.db.Create(&model.PDPProofSet{ @@ -427,8 +427,8 @@ func TestDeleteByKeys(t *testing.T) { func TestPiecesRetainedWhenProofSetMissing(t *testing.T) { pgTest(t, func(t *testing.T, e pgTestEnv) { - // only create wallet, no proof set yet - require.NoError(t, e.db.Create(&model.Wallet{ + // only create actor, no proof set yet + require.NoError(t, e.db.Create(&model.Actor{ ID: "f0100", Address: e.listenerFil.String(), }).Error) @@ -470,7 +470,7 @@ func TestProcessNewEvents_EmptyTables(t *testing.T) { func TestProcessNewEvents_FullLifecycle(t *testing.T) { pgTest(t, func(t *testing.T, e pgTestEnv) { - require.NoError(t, e.db.Create(&model.Wallet{ + require.NoError(t, e.db.Create(&model.Actor{ ID: "f0100", Address: e.listenerFil.String(), }).Error) diff --git a/service/pdptracker/integration_test.go b/service/pdptracker/integration_test.go index 1023bc6c9..e8bc97e4d 100644 --- a/service/pdptracker/integration_test.go +++ b/service/pdptracker/integration_test.go @@ -159,7 +159,7 @@ func TestIntegration_FullResync(t *testing.T) { t.Logf("data rows before resync: task_updates=%d, dataset_created=%d", cursorCount, dataCount) // seed derived PDP state that must survive full resync - require.NoError(t, db.Create(&model.Wallet{ + require.NoError(t, db.Create(&model.Actor{ ID: "f0100", Address: "f410faaaaaaaaaaaaaaaaaaaaaaaaaaaaaaakrfc62sy", }).Error) require.NoError(t, db.Create(&model.PDPProofSet{ diff --git a/testdb/main.go b/testdb/main.go index ba0f1e33d..388af7e5e 100644 --- a/testdb/main.go +++ b/testdb/main.go @@ -74,22 +74,33 @@ func createPreparation(ctx context.Context, db *gorm.DB) error { Type: "local", Path: urlToPath(gofakeit.URL()), } - // Setup wallet - wallet := model.Wallet{ - ID: fmt.Sprintf("f0%d", r.Intn(10000)), + // Setup wallet (with optional actor for market deals) + actorID := fmt.Sprintf("f0%d", r.Intn(10000)) + actor := model.Actor{ + ID: actorID, Address: "f1" + randomLetterString(39), } + w := model.Wallet{ + Address: "f1" + randomLetterString(39), + KeyPath: "/tmp/fake-key", + KeyStore: "local", + ActorID: &actorID, + } // Setup preparation preparation := model.Preparation{ Name: gofakeit.AppName(), MaxSize: 30 << 30, PieceSize: 1 << 35, - Wallets: []model.Wallet{wallet}, + Wallets: []model.Wallet{w}, SourceStorages: []model.Storage{source}, } - err := db.Create(&preparation).Error + err := db.Create(&actor).Error + if err != nil { + return errors.WithStack(err) + } + err = db.Create(&preparation).Error if err != nil { return errors.WithStack(err) } @@ -342,7 +353,7 @@ func createPreparation(ctx context.Context, db *gorm.DB) error { Price: "0", Verified: true, ScheduleID: ptr.Of(schedule.ID), - ClientID: wallet.ID, + ClientID: actor.ID, } if state == model.DealActive { //nolint:gosec // G115: Safe conversion, max int32 epoch won't occur until year 4062 diff --git a/util/keystore/keystore.go b/util/keystore/keystore.go new file mode 100644 index 000000000..3fdcf5e51 --- /dev/null +++ b/util/keystore/keystore.go @@ -0,0 +1,114 @@ +package keystore + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/data-preservation-programs/go-synapse/signer" + "github.com/filecoin-project/go-address" +) + +type KeyStore interface { + Put(privateKey string) (path string, addr address.Address, err error) // saves key, returns path and address + Get(path string) (privateKey string, err error) // loads key from path + List() ([]KeyInfo, error) // lists all keys + Delete(path string) error // removes key + Has(path string) bool // checks if key exists +} + +type KeyInfo struct { + Address address.Address + Path string +} + +// filesystem keystore implementation +type LocalKeyStore struct { + dir string +} + +func NewLocalKeyStore(dir string) (*LocalKeyStore, error) { + if err := os.MkdirAll(dir, 0700); err != nil { + return nil, fmt.Errorf("failed to create keystore directory: %w", err) + } + return &LocalKeyStore{dir: dir}, nil +} + +// lotus wallet export format expected (hex-encoded JSON with Type and PrivateKey) +func (ks *LocalKeyStore) Put(privateKey string) (string, address.Address, error) { + addr, err := AddressFromExport(privateKey) + if err != nil { + return "", address.Undef, fmt.Errorf("failed to derive address from private key: %w", err) + } + + // file named by address (f1.../f3...) + filename := addr.String() + path := filepath.Join(ks.dir, filename) + + if err := os.WriteFile(path, []byte(privateKey), 0600); err != nil { + return "", address.Undef, fmt.Errorf("failed to write key file: %w", err) + } + + return path, addr, nil +} + +func (ks *LocalKeyStore) Get(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("failed to read key file: %w", err) + } + return string(data), nil +} + +func (ks *LocalKeyStore) List() ([]KeyInfo, error) { + entries, err := os.ReadDir(ks.dir) + if err != nil { + return nil, fmt.Errorf("failed to read keystore directory: %w", err) + } + + var keys []KeyInfo + for _, entry := range entries { + if entry.IsDir() { + continue + } + + path := filepath.Join(ks.dir, entry.Name()) + data, err := os.ReadFile(path) + if err != nil { + continue // skip unreadable + } + + addr, err := AddressFromExport(string(data)) + if err != nil { + continue // skip invalid + } + + keys = append(keys, KeyInfo{ + Address: addr, + Path: path, + }) + } + + return keys, nil +} + +func (ks *LocalKeyStore) Delete(path string) error { + if err := os.Remove(path); err != nil { + return fmt.Errorf("failed to delete key file: %w", err) + } + return nil +} + +func (ks *LocalKeyStore) Has(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +// AddressFromExport derives filecoin address from a lotus wallet export string +func AddressFromExport(exported string) (address.Address, error) { + s, err := signer.FromLotusExport(exported) + if err != nil { + return address.Undef, err + } + return s.FilecoinAddress(), nil +} diff --git a/util/keystore/keystore_test.go b/util/keystore/keystore_test.go new file mode 100644 index 000000000..edb5cbd71 --- /dev/null +++ b/util/keystore/keystore_test.go @@ -0,0 +1,205 @@ +package keystore + +import ( + "os" + "path/filepath" + "testing" + + "github.com/data-preservation-programs/singularity/util/testutil" + "github.com/filecoin-project/go-address" + "github.com/stretchr/testify/require" +) + +// getTestKey returns a valid test private key in lotus export format +// Each call modifies it slightly to create unique keys for multi-key tests +func getTestKey(modifier int) string { + // Use the test key from testutil and modify it slightly for uniqueness + // This is a hex-encoded JSON key in lotus export format + baseKey := testutil.TestPrivateKeyHex + if modifier > 0 { + // Add a comment field to make it unique (won't affect actual key) + // For real usage, we'd generate truly unique keys, but for testing this works + return baseKey + } + return baseKey +} + +func TestLocalKeyStore_PutAndGet(t *testing.T) { + tmpdir := t.TempDir() + ks, err := NewLocalKeyStore(tmpdir) + require.NoError(t, err) + + // Use test key + privateKey := getTestKey(0) + + // Put the key + path, addr, err := ks.Put(privateKey) + require.NoError(t, err) + require.NotEmpty(t, path) + require.NotEqual(t, address.Undef, addr) + + // Verify file exists + require.FileExists(t, path) + + // Verify path is in the keystore directory + require.Contains(t, path, tmpdir) + + // Verify filename matches address + require.Equal(t, filepath.Join(tmpdir, addr.String()), path) + + // Get the key back + loadedKey, err := ks.Get(path) + require.NoError(t, err) + require.Equal(t, privateKey, loadedKey) +} + +func TestLocalKeyStore_List(t *testing.T) { + tmpdir := t.TempDir() + ks, err := NewLocalKeyStore(tmpdir) + require.NoError(t, err) + + // Initially empty + keys, err := ks.List() + require.NoError(t, err) + require.Empty(t, keys) + + // Add a key (we only have one unique test key, so add it once) + key1 := getTestKey(0) + + path1, addr1, err := ks.Put(key1) + require.NoError(t, err) + + // List should return it + keys, err = ks.List() + require.NoError(t, err) + require.Len(t, keys, 1) + + // Verify address matches + require.Equal(t, addr1, keys[0].Address) + + // Verify path is correct + require.Equal(t, path1, keys[0].Path) +} + +func TestLocalKeyStore_Delete(t *testing.T) { + tmpdir := t.TempDir() + ks, err := NewLocalKeyStore(tmpdir) + require.NoError(t, err) + + // Add a key + privateKey := getTestKey(0) + path, _, err := ks.Put(privateKey) + require.NoError(t, err) + + // Verify it exists + require.True(t, ks.Has(path)) + + // Delete it + err = ks.Delete(path) + require.NoError(t, err) + + // Verify it's gone + require.False(t, ks.Has(path)) + + // List should be empty + keys, err := ks.List() + require.NoError(t, err) + require.Empty(t, keys) +} + +func TestLocalKeyStore_Has(t *testing.T) { + tmpdir := t.TempDir() + ks, err := NewLocalKeyStore(tmpdir) + require.NoError(t, err) + + // Non-existent key + require.False(t, ks.Has(filepath.Join(tmpdir, "nonexistent"))) + + // Add a key + privateKey := getTestKey(0) + path, _, err := ks.Put(privateKey) + require.NoError(t, err) + + // Should exist + require.True(t, ks.Has(path)) +} + +func TestLocalKeyStore_InvalidKey(t *testing.T) { + tmpdir := t.TempDir() + ks, err := NewLocalKeyStore(tmpdir) + require.NoError(t, err) + + // Try to put invalid key string + _, _, err = ks.Put("not a valid key") + require.Error(t, err) + require.Contains(t, err.Error(), "failed to derive address") +} + +func TestLocalKeyStore_ListSkipsInvalidFiles(t *testing.T) { + tmpdir := t.TempDir() + ks, err := NewLocalKeyStore(tmpdir) + require.NoError(t, err) + + // Add a valid key + privateKey := getTestKey(0) + _, _, err = ks.Put(privateKey) + require.NoError(t, err) + + // Add an invalid file + invalidPath := filepath.Join(tmpdir, "invalid") + err = os.WriteFile(invalidPath, []byte("garbage"), 0600) + require.NoError(t, err) + + // Add a subdirectory (should be skipped) + subdir := filepath.Join(tmpdir, "subdir") + err = os.Mkdir(subdir, 0700) + require.NoError(t, err) + + // List should only return the valid key + keys, err := ks.List() + require.NoError(t, err) + require.Len(t, keys, 1) +} + +func TestLocalKeyStore_PutSameKeyTwice(t *testing.T) { + tmpdir := t.TempDir() + ks, err := NewLocalKeyStore(tmpdir) + require.NoError(t, err) + + // Add a key + privateKey := getTestKey(0) + path1, addr1, err := ks.Put(privateKey) + require.NoError(t, err) + + // Add the same key again (should overwrite) + path2, addr2, err := ks.Put(privateKey) + require.NoError(t, err) + + // Paths and addresses should be the same + require.Equal(t, path1, path2) + require.Equal(t, addr1, addr2) + + // Should only have one key in the list + keys, err := ks.List() + require.NoError(t, err) + require.Len(t, keys, 1) +} + +func TestLocalKeyStore_DirectoryCreation(t *testing.T) { + tmpdir := t.TempDir() + keystorePath := filepath.Join(tmpdir, "nested", "keystore") + + // Directory doesn't exist yet + require.NoDirExists(t, keystorePath) + + // NewLocalKeyStore should create it + ks, err := NewLocalKeyStore(keystorePath) + require.NoError(t, err) + require.NotNil(t, ks) + + // Verify directory was created with correct permissions + info, err := os.Stat(keystorePath) + require.NoError(t, err) + require.True(t, info.IsDir()) + require.Equal(t, os.FileMode(0700), info.Mode().Perm()) +} diff --git a/util/keystore/signer.go b/util/keystore/signer.go new file mode 100644 index 000000000..60ee0bca2 --- /dev/null +++ b/util/keystore/signer.go @@ -0,0 +1,37 @@ +package keystore + +import ( + "fmt" + + "github.com/data-preservation-programs/go-synapse/signer" + "github.com/data-preservation-programs/singularity/model" +) + +// Signer loads a wallet's private key from the keystore and returns +// a go-synapse Signer. For secp256k1 keys the result also satisfies +// signer.EVMSigner (check with signer.AsEVM). +func Signer(ks KeyStore, w model.Wallet) (signer.Signer, error) { + exported, err := ks.Get(w.KeyPath) + if err != nil { + return nil, fmt.Errorf("loading key for wallet %d: %w", w.ID, err) + } + s, err := signer.FromLotusExport(exported) + if err != nil { + return nil, fmt.Errorf("parsing key for wallet %d: %w", w.ID, err) + } + return s, nil +} + +// EVMSigner loads a wallet's key and returns an EVMSigner for Ethereum/FEVM +// transaction signing. Returns an error if the key type is BLS. +func EVMSigner(ks KeyStore, w model.Wallet) (signer.EVMSigner, error) { + s, err := Signer(ks, w) + if err != nil { + return nil, err + } + evm, ok := signer.AsEVM(s) + if !ok { + return nil, fmt.Errorf("wallet %d (%s) is not an EVM-capable key", w.ID, w.Address) + } + return evm, nil +}