From 70f66a4ad4324a07ed391027b4b9b397d4559e73 Mon Sep 17 00:00:00 2001 From: optclblast Date: Tue, 28 May 2024 00:29:05 +0300 Subject: [PATCH] new payroll --- backend/README.md | 1 - backend/internal/factory/interactors.go | 2 + backend/internal/factory/wire_gen.go | 2 +- .../interface/rest/controllers/chain.go | 67 +++- backend/internal/interface/rest/domain/dto.go | 11 +- backend/internal/interface/rest/server.go | 22 +- backend/internal/pkg/models/models.go | 8 + .../usecase/interactors/chain/chain.go | 367 ++++++++++++++---- .../interactors/organizations/interactor.go | 2 + .../interactors/transactions/interactor.go | 20 +- .../repository/transactions/repository.go | 97 +++++ backend/migrations/blockd.sql | 22 +- 12 files changed, 527 insertions(+), 94 deletions(-) diff --git a/backend/README.md b/backend/README.md index 405a80b..36a4e8f 100644 --- a/backend/README.md +++ b/backend/README.md @@ -534,7 +534,6 @@ Response: } ``` -# Deprecated ## GET **/{organization_id}/transactions** Feth txs ### Request body: diff --git a/backend/internal/factory/interactors.go b/backend/internal/factory/interactors.go index 16f70c1..4f00697 100644 --- a/backend/internal/factory/interactors.go +++ b/backend/internal/factory/interactors.go @@ -44,11 +44,13 @@ func provideTxInteractor( log *slog.Logger, txRepo txRepo.Repository, orgInteractor organizations.OrganizationsInteractor, + chainInteractor chain.ChainInteractor, ) transactions.TransactionsInteractor { return transactions.NewTransactionsInteractor( log.WithGroup("transaction-interactor"), txRepo, orgInteractor, + chainInteractor, ) } diff --git a/backend/internal/factory/wire_gen.go b/backend/internal/factory/wire_gen.go index 9ed2b0d..a45a9a1 100644 --- a/backend/internal/factory/wire_gen.go +++ b/backend/internal/factory/wire_gen.go @@ -34,7 +34,7 @@ func ProvideService(c config.Config) (service.Service, func(), error) { authController := provideAuthController(logger, usersInteractor, authPresenter, jwtInteractor, authRepository, organizationsInteractor) organizationsPresenter := provideOrganizationsPresenter() organizationsController := provideOrganizationsController(logger, organizationsInteractor, organizationsPresenter) - transactionsInteractor := provideTxInteractor(logger, transactionsRepository, organizationsInteractor) + transactionsInteractor := provideTxInteractor(logger, transactionsRepository, organizationsInteractor, chainInteractor) transactionsController := provideTxController(logger, transactionsInteractor, chainInteractor, organizationsInteractor) participantsController := provideParticipantsController(logger, organizationsInteractor, usersInteractor) rootController := provideControllers(logger, authController, organizationsController, transactionsController, participantsController) diff --git a/backend/internal/interface/rest/controllers/chain.go b/backend/internal/interface/rest/controllers/chain.go index 3d13fb6..5bbf49a 100644 --- a/backend/internal/interface/rest/controllers/chain.go +++ b/backend/internal/interface/rest/controllers/chain.go @@ -272,9 +272,72 @@ func (s *transactionsController) ListMultisigs(w http.ResponseWriter, r *http.Re return s.txPresenter.ResponseMultisigs(r.Context(), msgs) } -// todo creates a new payout func (c *transactionsController) NewPayroll(w http.ResponseWriter, r *http.Request) ([]byte, error) { - return nil, nil + req, err := presenters.CreateRequest[domain.NewPayrollRequest](r) + if err != nil { + return nil, fmt.Errorf("error build request. %w", err) + } + + ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) + defer cancel() + + organizationID, err := ctxmeta.OrganizationId(ctx) + if err != nil { + return nil, fmt.Errorf("erropr fetch organization id from context. %w", err) + } + + c.log.Debug( + "NewPayrollRequest", + slog.Any("req", req), + slog.String("org id", organizationID.String()), + ) + + multisigID, err := uuid.Parse(req.MultisigID) + if err != nil { + return nil, fmt.Errorf("error invalid ") + } + + user, err := ctxmeta.User(ctx) + if err != nil { + return nil, fmt.Errorf("error fetch user from context. %w", err) + } + + userParticipant, err := c.organizationsInteractor.Participant(ctx, organizations.ParticipantParams{ + ID: user.Id(), + OrganizationID: organizationID, + // TODO fetch REAL first admin + }) + if err != nil { + return nil, fmt.Errorf("error fetch user protocpant. %w", err) + } + + if !userParticipant.IsOwner() { + return nil, fmt.Errorf("only owner can create payrolls") + } + + firstAdmin, err := c.organizationsInteractor.Participant(ctx, organizations.ParticipantParams{ + ID: user.Id(), + OrganizationID: organizationID, + // TODO fetch REAL first admin + }) + if err != nil { + return nil, fmt.Errorf("error fetch first admin. %w", err) + } + + if !firstAdmin.IsOwner() { + return nil, fmt.Errorf("invalid first admin. not owner") + } + + err = c.chainInteractor.PayrollDeploy(ctx, chain.PayrollDeployParams{ + MultisigID: multisigID, + FirstAdmin: firstAdmin, + Title: req.Title, + }) + if err != nil { + return nil, fmt.Errorf("error create new payroll contract. %w", err) + } + + return presenters.ResponseOK() } func (c *transactionsController) ConfirmPayroll(w http.ResponseWriter, r *http.Request) ([]byte, error) { diff --git a/backend/internal/interface/rest/domain/dto.go b/backend/internal/interface/rest/domain/dto.go index ae9e1a4..f1fcbf0 100644 --- a/backend/internal/interface/rest/domain/dto.go +++ b/backend/internal/interface/rest/domain/dto.go @@ -45,6 +45,10 @@ type LoginResponse struct { RTExpiredAt int64 `json:"refresh_token_expired_at"` } +type NewInviteLinkRequest struct { + ExpirationDate int `json:"expiration_date"` +} + // Organizations type NewOrganizationRequest struct { @@ -102,6 +106,8 @@ type AddEmployeeRequest struct { WalletAddress string `json:"wallet_address"` } +// Chain + type NewMultisigRequest struct { Title string `json:"title"` Owners []struct { @@ -110,6 +116,7 @@ type NewMultisigRequest struct { Confirmations int `json:"confirmations"` } -type NewInviteLinkRequest struct { - ExpirationDate int `json:"expiration_date"` +type NewPayrollRequest struct { + MultisigID string `json:"multisig_id"` + Title string `json:"title"` } diff --git a/backend/internal/interface/rest/server.go b/backend/internal/interface/rest/server.go index 427483c..809cdca 100644 --- a/backend/internal/interface/rest/server.go +++ b/backend/internal/interface/rest/server.go @@ -90,7 +90,7 @@ func (s *Server) buildRouter() { router.Use(render.SetContentType(render.ContentTypeJSON)) - router.Get("/ping", s.handle(s.controllers.Ping.Ping, "ping")) + router.Get("/ping", s.handle(s.controllers.Ping.Ping, "ping")) // DEBUG router.Post("/join", s.handle(s.controllers.Auth.Join, "join")) router.Post("/login", s.handle(s.controllers.Auth.Login, "login")) @@ -108,16 +108,6 @@ func (s *Server) buildRouter() { r.Post("/", s.handle(s.controllers.Organizations.NewOrganization, "new_organization")) r.Route("/{organization_id}", func(r chi.Router) { - // Deprecated?? - r.Route("/transactions", func(r chi.Router) { - r.Get("/", s.handle(s.controllers.Transactions.List, "tx_list")) - r.Post("/", s.handle(s.controllers.Transactions.New, "new_tx")) - r.Put( - "/{tx_id}", - s.handle(s.controllers.Transactions.UpdateStatus, "update_tx_status"), - ) - }) - r.Route("/payrolls", func(r chi.Router) { r.Get("/", s.handle(s.controllers.Transactions.ListPayrolls, "list_payrolls")) r.Post("/", s.handle(s.controllers.Transactions.NewPayroll, "new_payroll")) @@ -144,6 +134,16 @@ func (s *Server) buildRouter() { r.Get("/", nil) // todo если успею }) }) + + // Deprecated?? + r.Route("/transactions", func(r chi.Router) { + r.Get("/", s.handle(s.controllers.Transactions.List, "tx_list")) + r.Post("/", s.handle(s.controllers.Transactions.New, "new_tx")) + r.Put( + "/{tx_id}", + s.handle(s.controllers.Transactions.UpdateStatus, "update_tx_status"), + ) + }) }) }) diff --git a/backend/internal/pkg/models/models.go b/backend/internal/pkg/models/models.go index fc88ca2..e28886a 100644 --- a/backend/internal/pkg/models/models.go +++ b/backend/internal/pkg/models/models.go @@ -23,3 +23,11 @@ type MultisigConfirmation struct { CreatedAt time.Time UpdatedAt time.Time } + +type Payroll struct { + ID uuid.UUID + Title string + Address []byte + OrganizationID uuid.UUID + MultisigID uuid.UUID +} diff --git a/backend/internal/usecase/interactors/chain/chain.go b/backend/internal/usecase/interactors/chain/chain.go index c7df1b4..e97388d 100644 --- a/backend/internal/usecase/interactors/chain/chain.go +++ b/backend/internal/usecase/interactors/chain/chain.go @@ -12,6 +12,7 @@ import ( "github.com/emochka2007/block-accounting/internal/pkg/config" "github.com/emochka2007/block-accounting/internal/pkg/ctxmeta" + "github.com/emochka2007/block-accounting/internal/pkg/logger" "github.com/emochka2007/block-accounting/internal/pkg/models" "github.com/emochka2007/block-accounting/internal/usecase/repository/transactions" "github.com/ethereum/go-ethereum/common" @@ -19,10 +20,12 @@ import ( ) type ChainInteractor interface { + PubKey(ctx context.Context, user *models.User) ([]byte, error) + NewMultisig(ctx context.Context, params NewMultisigParams) error ListMultisigs(ctx context.Context, params ListMultisigsParams) ([]models.Multisig, error) - PubKey(ctx context.Context, user *models.User) ([]byte, error) - SalaryDeploy(ctx context.Context, firtsAdmin models.OrganizationParticipant) error + + PayrollDeploy(ctx context.Context, params PayrollDeployParams) error } type chainInteractor struct { @@ -90,70 +93,138 @@ func (i *chainInteractor) NewMultisig(ctx context.Context, params NewMultisigPar return fmt.Errorf("error fetch organization id from context. %w", err) } - body := bytes.NewBuffer(requestBody) + go func() { + pid := uuid.Must(uuid.NewV7()).String() + startTime := time.Now() - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body) - if err != nil { - return fmt.Errorf("error build request. %w", err) - } - - req.Header.Add("Content-Type", "application/json") - req.Header.Add("X-Seed", common.Bytes2Hex(user.Seed())) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - i.log.Error( - "error send deploy multisig request", - slog.String("endpoint", endpoint), - slog.Any("params", params), + i.log.Info( + "new multisig worker started", + slog.String("pid", pid), ) - return fmt.Errorf("error build new multisig request. %w", err) - } + doneCh := make(chan struct{}) - defer resp.Body.Close() + defer func() { + if err := recover(); err != nil { + i.log.Error("worker paniced!", slog.Any("panic", err)) + } - raw, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("error read body. %w", err) - } + doneCh <- struct{}{} + close(doneCh) + }() - respObject := new(newMultisigChainResponse) + go func() { + warn := time.After(1 * time.Minute) + select { + case <-doneCh: + i.log.Info( + "new multisig worker done", + slog.String("pid", pid), + slog.Time("started at", startTime), + slog.Time("done at", time.Now()), + slog.Duration("work time", time.Since(startTime)), + ) + case <-warn: + i.log.Warn( + "new multisig worker seems sleeping", + slog.String("pid", pid), + slog.Duration("work time", time.Since(startTime)), + ) + } + }() - if err := json.Unmarshal(raw, &respObject); err != nil { - return fmt.Errorf("error parse chain-api response body. %w", err) - } + requestContext, cancel := context.WithTimeout(context.TODO(), time.Minute*15) + defer cancel() - if respObject.Address == "" { - return fmt.Errorf("error multisig address is empty") - } + body := bytes.NewBuffer(requestBody) - multisigAddress := common.Hex2Bytes(respObject.Address[2:]) + req, err := http.NewRequestWithContext(requestContext, http.MethodPost, endpoint, body) + if err != nil { + i.log.Error( + "error build request", + logger.Err(err), + ) - createdAt := time.Now() + return + } - msg := models.Multisig{ - ID: uuid.Must(uuid.NewV7()), - Title: params.Title, - Address: multisigAddress, - OrganizationID: organizationID, - Owners: params.Owners, - ConfirmationsRequired: params.Confirmations, - CreatedAt: createdAt, - UpdatedAt: createdAt, - } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("X-Seed", common.Bytes2Hex(user.Seed())) - i.log.Debug( - "deploy multisig response", - slog.Int("code", resp.StatusCode), - slog.String("body", string(raw)), - slog.Any("parsed", respObject), - slog.Any("multisig object", msg), - ) + resp, err := http.DefaultClient.Do(req) + if err != nil { + i.log.Error( + "error send deploy multisig request", + slog.String("endpoint", endpoint), + slog.Any("params", params), + ) - if err := i.txRepository.AddMultisig(ctx, msg); err != nil { - return fmt.Errorf("error add new multisig. %w", err) - } + return + } + + defer resp.Body.Close() + + raw, err := io.ReadAll(resp.Body) + if err != nil { + i.log.Error( + "error read body", + logger.Err(err), + ) + + return + } + + respObject := new(newMultisigChainResponse) + + if err := json.Unmarshal(raw, &respObject); err != nil { + i.log.Error( + "error parse chain-api response body", + logger.Err(err), + ) + + return + } + + if respObject.Address == "" { + i.log.Error( + "error multisig address is empty", + ) + + return + } + + multisigAddress := common.Hex2Bytes(respObject.Address[2:]) + + createdAt := time.Now() + + msg := models.Multisig{ + ID: uuid.Must(uuid.NewV7()), + Title: params.Title, + Address: multisigAddress, + OrganizationID: organizationID, + Owners: params.Owners, + ConfirmationsRequired: params.Confirmations, + CreatedAt: createdAt, + UpdatedAt: createdAt, + } + + i.log.Debug( + "deploy multisig response", + slog.Int("code", resp.StatusCode), + slog.String("body", string(raw)), + slog.Any("parsed", respObject), + slog.Any("multisig object", msg), + ) + + if err := i.txRepository.AddMultisig(requestContext, msg); err != nil { + i.log.Error( + "error add new multisig", + logger.Err(err), + ) + + return + } + }() return nil } @@ -194,46 +265,203 @@ func (i *chainInteractor) PubKey(ctx context.Context, user *models.User) ([]byte return common.Hex2Bytes(pubKeyStr), nil } -func (i *chainInteractor) SalaryDeploy(ctx context.Context, firtsAdmin models.OrganizationParticipant) error { +type PayrollDeployParams struct { + FirstAdmin models.OrganizationParticipant + MultisigID uuid.UUID + Title string +} + +type newPayrollContractChainResponse struct { + Address string `json:"address"` +} + +func (i *chainInteractor) PayrollDeploy( + ctx context.Context, + params PayrollDeployParams, +) error { user, err := ctxmeta.User(ctx) if err != nil { return fmt.Errorf("error fetch user from context. %w", err) } - if user.Id() != firtsAdmin.Id() || firtsAdmin.GetUser() == nil { + if user.Id() != params.FirstAdmin.Id() || params.FirstAdmin.GetUser() == nil { return fmt.Errorf("error unauthorized access") } + organizationID, err := ctxmeta.OrganizationId(ctx) + if err != nil { + return fmt.Errorf("error fetch organization id from context. %w", err) + } + + multisigs, err := i.ListMultisigs(ctx, ListMultisigsParams{ + OrganizationID: organizationID, + IDs: uuid.UUIDs{params.MultisigID}, + }) + if err != nil { + return fmt.Errorf("error fetch multisigs by id. %w", err) + } + + if len(multisigs) == 0 { + return fmt.Errorf("error empty multisigs set") + } + + i.log.Debug( + "PayrollDeploy", + slog.String("organization id", organizationID.String()), + slog.String("multisig id", params.MultisigID.String()), + slog.String("multisig address", common.Bytes2Hex(multisigs[0].Address)), + slog.String("X-Seed header data", common.Bytes2Hex(user.Seed())), + ) + + maddr := common.Bytes2Hex(multisigs[0].Address) + + if maddr == "" { + return fmt.Errorf("empty multisig address") + } + + if maddr[0] != 0 && maddr[1] != 'x' { + maddr = "0x" + maddr + } + requestBody, err := json.Marshal(map[string]any{ - "authorizedWallet": common.Bytes2Hex(user.Seed()), + "authorizedWallet": maddr, }) if err != nil { return fmt.Errorf("error marshal request body. %w", err) } - body := bytes.NewBuffer(requestBody) + go func() { + pid := uuid.Must(uuid.NewV7()).String() + startTime := time.Now() - endpoint := i.config.ChainAPI.Host + "/salaries/deploy" + i.log.Info( + "new paroll worker started", + slog.String("pid", pid), + ) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body) - if err != nil { - return fmt.Errorf("error build request. %w", err) - } + doneCh := make(chan struct{}) - req.Header.Add("Content-Type", "application/json") - req.Header.Add("X-Seed", common.Bytes2Hex(user.Seed())) + defer func() { + if err := recover(); err != nil { + i.log.Error("worker paniced!", slog.Any("panic", err)) + } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return fmt.Errorf("error fetch deploy salary contract. %w", err) - } + doneCh <- struct{}{} + close(doneCh) + }() - defer resp.Body.Close() + go func() { + warn := time.After(2 * time.Minute) + select { + case <-doneCh: + i.log.Info( + "new paroll worker done", + slog.String("pid", pid), + slog.Time("started at", startTime), + slog.Time("done at", time.Now()), + slog.Duration("work time", time.Since(startTime)), + ) + case <-warn: + i.log.Warn( + "new paroll worker seems sleeping", + slog.String("pid", pid), + slog.Duration("work time", time.Since(startTime)), + ) + } + }() + + requestContext, cancel := context.WithTimeout(context.TODO(), time.Minute*20) + defer cancel() + + body := bytes.NewBuffer(requestBody) + + endpoint := i.config.ChainAPI.Host + "/salaries/deploy" + + i.log.Debug( + "request", + slog.String("body", string(requestBody)), + slog.String("endpoint", endpoint), + ) + + req, err := http.NewRequestWithContext(requestContext, http.MethodPost, endpoint, body) + if err != nil { + i.log.Error( + "error build request", + logger.Err(fmt.Errorf("error build request. %w", err)), + ) + return + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("X-Seed", common.Bytes2Hex(user.Seed())) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + i.log.Error( + "error fetch deploy salary contract", + logger.Err(err), + ) + + return + } + + defer resp.Body.Close() + + raw, err := io.ReadAll(resp.Body) + if err != nil { + i.log.Error( + "error read body", + logger.Err(err), + ) + + return + } + + respObject := new(newPayrollContractChainResponse) + + if err := json.Unmarshal(raw, &respObject); err != nil { + i.log.Error( + "error parse chain-api response body", + logger.Err(err), + ) + + return + } + + if respObject.Address == "" { + i.log.Error( + "error multisig address is empty", + ) + + return + } + + addr := common.Hex2Bytes(respObject.Address[2:]) + + createdAt := time.Now() + + if err := i.txRepository.AddPayrollContract(requestContext, transactions.AddPayrollContract{ + ID: uuid.Must(uuid.NewV7()), + Title: params.Title, + Address: addr, + OrganizationID: organizationID, + MultisigID: params.MultisigID, + CreatedAt: createdAt, + }); err != nil { + i.log.Error( + "error add new payroll contract", + logger.Err(err), + ) + + return + } + }() return nil } type ListMultisigsParams struct { + IDs uuid.UUIDs OrganizationID uuid.UUID } @@ -242,6 +470,7 @@ func (i *chainInteractor) ListMultisigs( params ListMultisigsParams, ) ([]models.Multisig, error) { multisigs, err := i.txRepository.ListMultisig(ctx, transactions.ListMultisigsParams{ + IDs: params.IDs, OrganizationID: params.OrganizationID, }) if err != nil { diff --git a/backend/internal/usecase/interactors/organizations/interactor.go b/backend/internal/usecase/interactors/organizations/interactor.go index 84f1bfc..d665531 100644 --- a/backend/internal/usecase/interactors/organizations/interactor.go +++ b/backend/internal/usecase/interactors/organizations/interactor.go @@ -44,6 +44,7 @@ type ParticipantParams struct { UsersOnly bool ActiveOnly bool EmployeesOnly bool + OwnerOnly bool } type ParticipantsParams struct { @@ -54,6 +55,7 @@ type ParticipantsParams struct { UsersOnly bool ActiveOnly bool EmployeesOnly bool + OwnerOnly bool } type OrganizationsInteractor interface { diff --git a/backend/internal/usecase/interactors/transactions/interactor.go b/backend/internal/usecase/interactors/transactions/interactor.go index 9ff734c..2be4b05 100644 --- a/backend/internal/usecase/interactors/transactions/interactor.go +++ b/backend/internal/usecase/interactors/transactions/interactor.go @@ -10,6 +10,7 @@ import ( "github.com/emochka2007/block-accounting/internal/pkg/ctxmeta" "github.com/emochka2007/block-accounting/internal/pkg/models" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/chain" "github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations" "github.com/emochka2007/block-accounting/internal/usecase/repository/transactions" "github.com/google/uuid" @@ -59,20 +60,23 @@ type TransactionsInteractor interface { } type transactionsInteractor struct { - log *slog.Logger - txRepo transactions.Repository - orgInteractor organizations.OrganizationsInteractor + log *slog.Logger + txRepo transactions.Repository + orgInteractor organizations.OrganizationsInteractor + chainInteractor chain.ChainInteractor } func NewTransactionsInteractor( log *slog.Logger, txRepo transactions.Repository, orgInteractor organizations.OrganizationsInteractor, + chainInteractor chain.ChainInteractor, ) TransactionsInteractor { return &transactionsInteractor{ - log: log, - txRepo: txRepo, - orgInteractor: orgInteractor, + log: log, + txRepo: txRepo, + orgInteractor: orgInteractor, + chainInteractor: chainInteractor, } } @@ -224,6 +228,8 @@ func (i *transactionsInteractor) Confirm(ctx context.Context, params ConfirmPara return nil, fmt.Errorf("error confirm transaction. %w", err) } + // TODO confirm tx via chain-api + tx, err := i.txRepo.GetTransactions(ctx, transactions.GetTransactionsParams{ Ids: uuid.UUIDs{params.TxID}, OrganizationId: params.OrganizationID, @@ -240,6 +246,8 @@ func (i *transactionsInteractor) Confirm(ctx context.Context, params ConfirmPara return tx[0], nil } +// TODO Execute() + func (i *transactionsInteractor) Cancel(ctx context.Context, params CancelParams) (*models.Transaction, error) { user, err := ctxmeta.User(ctx) if err != nil { diff --git a/backend/internal/usecase/repository/transactions/repository.go b/backend/internal/usecase/repository/transactions/repository.go index bbe691a..dc7c228 100644 --- a/backend/internal/usecase/repository/transactions/repository.go +++ b/backend/internal/usecase/repository/transactions/repository.go @@ -54,6 +54,9 @@ type Repository interface { AddMultisig(ctx context.Context, multisig models.Multisig) error ListMultisig(ctx context.Context, params ListMultisigsParams) ([]models.Multisig, error) + ConfirmMultisig(ctx context.Context, params ConfirmMultisigParams) error + + AddPayrollContract(ctx context.Context, params AddPayrollContract) error } type repositorySQL struct { @@ -478,6 +481,12 @@ func (r *repositorySQL) ListMultisig( "organization_id": params.OrganizationID, }).PlaceholderFormat(sq.Dollar) + if len(params.IDs) > 0 { + query = query.Where(sq.Eq{ + "id": params.IDs, + }) + } + rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx) if err != nil { return fmt.Errorf("error fetch multisigs from database. %w", err) @@ -543,6 +552,94 @@ func (r *repositorySQL) ListMultisig( return msgs, nil } +type ConfirmMultisigParams struct { + MultisigID uuid.UUID + OrganizationsID uuid.UUID + CinfirmedBy *models.OrganizationUser + ConfirmedAt time.Time +} + +func (r *repositorySQL) ConfirmMultisig(ctx context.Context, params ConfirmMultisigParams) error { + return sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + deleteOldQuery := sq.Delete("multisig_confirmations"). + Where(sq.Eq{ + "multisig_id": params.MultisigID, + "owner_id": params.CinfirmedBy.Id(), + }). + PlaceholderFormat(sq.Dollar) + + if _, err := deleteOldQuery.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error delete old multisig confirmation. %w", err) + } + + query := sq.Insert("multisig_confirmations"). + Columns( + "multisig_id", + "owner_id", + "created_at", + ). + Values( + params.MultisigID, + params.CinfirmedBy.Id(), + params.ConfirmedAt, + ). + PlaceholderFormat(sq.Dollar) + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error add multisig confirmation. %w", err) + } + + return nil + }) +} + +type AddPayrollContract struct { + ID uuid.UUID + Title string + Description string + Address []byte + Payload []byte + OrganizationID uuid.UUID + MultisigID uuid.UUID + CreatedAt time.Time +} + +func (r *repositorySQL) AddPayrollContract( + ctx context.Context, + params AddPayrollContract, +) error { + return sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + query := sq.Insert("payrolls"). + Columns( + "id", + "title", + "description", + "address", + "payload", + "organization_id", + "multisig_id", + "created_at", + ). + Values( + params.ID, + params.Title, + params.Description, + params.Address, + params.Payload, + params.OrganizationID, + params.MultisigID, + params.CreatedAt, + ). + PlaceholderFormat(sq.Dollar) + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error add new payroll contract. %w", err) + } + + return nil + }) +} + type fetchOwnersParams struct { MultisigID uuid.UUID OrganizationID uuid.UUID diff --git a/backend/migrations/blockd.sql b/backend/migrations/blockd.sql index 13a664c..12ec28c 100644 --- a/backend/migrations/blockd.sql +++ b/backend/migrations/blockd.sql @@ -99,7 +99,7 @@ create table if not exists transactions ( amount decimal default 0, to_addr bytea not null, - tx_index bytea default null, + tx_index bigint default 0, max_fee_allowed decimal default 0, deadline timestamp default null, @@ -193,6 +193,9 @@ create index if not exists idx_multisig_confirmations_owners_multisig_id create index if not exists idx_multisig_confirmations_owners_owner_id on multisig_confirmations (owner_id); +create index if not exists idx_multisig_confirmations_owners_multisig_id_owner_id + on multisig_confirmations (multisig_id, owner_id); + create table invites ( link_hash varchar(64) primary key, organization_id uuid, @@ -200,4 +203,19 @@ create table invites ( created_at timestamp default current_timestamp, expired_at timestamp default null, used_at timestamp default null -); \ No newline at end of file +); + +create table payrolls ( + id uuid primary key, + title varchar(250) default 'New Payroll', + description text not null, + address bytea not null, + payload bytea default null, + organization_id uuid not null references organizations(id), + tx_index bytea default null, + multisig_id uuid references multisigs(id), + created_at timestamp default current_timestamp, + updated_at timestamp default current_timestamp +); + +create table payrolls \ No newline at end of file