new payroll

This commit is contained in:
r8zavetr8v 2024-05-28 00:29:05 +03:00
parent 4b85cdf811
commit 70f66a4ad4
12 changed files with 527 additions and 94 deletions

View File

@ -534,7 +534,6 @@ Response:
} }
``` ```
# Deprecated
## GET **/{organization_id}/transactions** ## GET **/{organization_id}/transactions**
Feth txs Feth txs
### Request body: ### Request body:

View File

@ -44,11 +44,13 @@ func provideTxInteractor(
log *slog.Logger, log *slog.Logger,
txRepo txRepo.Repository, txRepo txRepo.Repository,
orgInteractor organizations.OrganizationsInteractor, orgInteractor organizations.OrganizationsInteractor,
chainInteractor chain.ChainInteractor,
) transactions.TransactionsInteractor { ) transactions.TransactionsInteractor {
return transactions.NewTransactionsInteractor( return transactions.NewTransactionsInteractor(
log.WithGroup("transaction-interactor"), log.WithGroup("transaction-interactor"),
txRepo, txRepo,
orgInteractor, orgInteractor,
chainInteractor,
) )
} }

View File

@ -34,7 +34,7 @@ func ProvideService(c config.Config) (service.Service, func(), error) {
authController := provideAuthController(logger, usersInteractor, authPresenter, jwtInteractor, authRepository, organizationsInteractor) authController := provideAuthController(logger, usersInteractor, authPresenter, jwtInteractor, authRepository, organizationsInteractor)
organizationsPresenter := provideOrganizationsPresenter() organizationsPresenter := provideOrganizationsPresenter()
organizationsController := provideOrganizationsController(logger, organizationsInteractor, organizationsPresenter) organizationsController := provideOrganizationsController(logger, organizationsInteractor, organizationsPresenter)
transactionsInteractor := provideTxInteractor(logger, transactionsRepository, organizationsInteractor) transactionsInteractor := provideTxInteractor(logger, transactionsRepository, organizationsInteractor, chainInteractor)
transactionsController := provideTxController(logger, transactionsInteractor, chainInteractor, organizationsInteractor) transactionsController := provideTxController(logger, transactionsInteractor, chainInteractor, organizationsInteractor)
participantsController := provideParticipantsController(logger, organizationsInteractor, usersInteractor) participantsController := provideParticipantsController(logger, organizationsInteractor, usersInteractor)
rootController := provideControllers(logger, authController, organizationsController, transactionsController, participantsController) rootController := provideControllers(logger, authController, organizationsController, transactionsController, participantsController)

View File

@ -272,9 +272,72 @@ func (s *transactionsController) ListMultisigs(w http.ResponseWriter, r *http.Re
return s.txPresenter.ResponseMultisigs(r.Context(), msgs) return s.txPresenter.ResponseMultisigs(r.Context(), msgs)
} }
// todo creates a new payout
func (c *transactionsController) NewPayroll(w http.ResponseWriter, r *http.Request) ([]byte, error) { 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) { func (c *transactionsController) ConfirmPayroll(w http.ResponseWriter, r *http.Request) ([]byte, error) {

View File

@ -45,6 +45,10 @@ type LoginResponse struct {
RTExpiredAt int64 `json:"refresh_token_expired_at"` RTExpiredAt int64 `json:"refresh_token_expired_at"`
} }
type NewInviteLinkRequest struct {
ExpirationDate int `json:"expiration_date"`
}
// Organizations // Organizations
type NewOrganizationRequest struct { type NewOrganizationRequest struct {
@ -102,6 +106,8 @@ type AddEmployeeRequest struct {
WalletAddress string `json:"wallet_address"` WalletAddress string `json:"wallet_address"`
} }
// Chain
type NewMultisigRequest struct { type NewMultisigRequest struct {
Title string `json:"title"` Title string `json:"title"`
Owners []struct { Owners []struct {
@ -110,6 +116,7 @@ type NewMultisigRequest struct {
Confirmations int `json:"confirmations"` Confirmations int `json:"confirmations"`
} }
type NewInviteLinkRequest struct { type NewPayrollRequest struct {
ExpirationDate int `json:"expiration_date"` MultisigID string `json:"multisig_id"`
Title string `json:"title"`
} }

View File

@ -90,7 +90,7 @@ func (s *Server) buildRouter() {
router.Use(render.SetContentType(render.ContentTypeJSON)) 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("/join", s.handle(s.controllers.Auth.Join, "join"))
router.Post("/login", s.handle(s.controllers.Auth.Login, "login")) 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.Post("/", s.handle(s.controllers.Organizations.NewOrganization, "new_organization"))
r.Route("/{organization_id}", func(r chi.Router) { 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.Route("/payrolls", func(r chi.Router) {
r.Get("/", s.handle(s.controllers.Transactions.ListPayrolls, "list_payrolls")) r.Get("/", s.handle(s.controllers.Transactions.ListPayrolls, "list_payrolls"))
r.Post("/", s.handle(s.controllers.Transactions.NewPayroll, "new_payroll")) r.Post("/", s.handle(s.controllers.Transactions.NewPayroll, "new_payroll"))
@ -144,6 +134,16 @@ func (s *Server) buildRouter() {
r.Get("/", nil) // todo если успею 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"),
)
})
}) })
}) })

View File

@ -23,3 +23,11 @@ type MultisigConfirmation struct {
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
} }
type Payroll struct {
ID uuid.UUID
Title string
Address []byte
OrganizationID uuid.UUID
MultisigID uuid.UUID
}

View File

@ -12,6 +12,7 @@ import (
"github.com/emochka2007/block-accounting/internal/pkg/config" "github.com/emochka2007/block-accounting/internal/pkg/config"
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta" "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/pkg/models"
"github.com/emochka2007/block-accounting/internal/usecase/repository/transactions" "github.com/emochka2007/block-accounting/internal/usecase/repository/transactions"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -19,10 +20,12 @@ import (
) )
type ChainInteractor interface { type ChainInteractor interface {
PubKey(ctx context.Context, user *models.User) ([]byte, error)
NewMultisig(ctx context.Context, params NewMultisigParams) error NewMultisig(ctx context.Context, params NewMultisigParams) error
ListMultisigs(ctx context.Context, params ListMultisigsParams) ([]models.Multisig, 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 { 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) 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) i.log.Info(
if err != nil { "new multisig worker started",
return fmt.Errorf("error build request. %w", err) slog.String("pid", pid),
}
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),
) )
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) doneCh <- struct{}{}
if err != nil { close(doneCh)
return fmt.Errorf("error read body. %w", err) }()
}
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 { requestContext, cancel := context.WithTimeout(context.TODO(), time.Minute*15)
return fmt.Errorf("error parse chain-api response body. %w", err) defer cancel()
}
if respObject.Address == "" { body := bytes.NewBuffer(requestBody)
return fmt.Errorf("error multisig address is empty")
}
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{ req.Header.Add("Content-Type", "application/json")
ID: uuid.Must(uuid.NewV7()), req.Header.Add("X-Seed", common.Bytes2Hex(user.Seed()))
Title: params.Title,
Address: multisigAddress,
OrganizationID: organizationID,
Owners: params.Owners,
ConfirmationsRequired: params.Confirmations,
CreatedAt: createdAt,
UpdatedAt: createdAt,
}
i.log.Debug( resp, err := http.DefaultClient.Do(req)
"deploy multisig response", if err != nil {
slog.Int("code", resp.StatusCode), i.log.Error(
slog.String("body", string(raw)), "error send deploy multisig request",
slog.Any("parsed", respObject), slog.String("endpoint", endpoint),
slog.Any("multisig object", msg), slog.Any("params", params),
) )
if err := i.txRepository.AddMultisig(ctx, msg); err != nil { return
return fmt.Errorf("error add new multisig. %w", err) }
}
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 return nil
} }
@ -194,46 +265,203 @@ func (i *chainInteractor) PubKey(ctx context.Context, user *models.User) ([]byte
return common.Hex2Bytes(pubKeyStr), nil 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) user, err := ctxmeta.User(ctx)
if err != nil { if err != nil {
return fmt.Errorf("error fetch user from context. %w", err) 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") 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{ requestBody, err := json.Marshal(map[string]any{
"authorizedWallet": common.Bytes2Hex(user.Seed()), "authorizedWallet": maddr,
}) })
if err != nil { if err != nil {
return fmt.Errorf("error marshal request body. %w", err) 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) doneCh := make(chan struct{})
if err != nil {
return fmt.Errorf("error build request. %w", err)
}
req.Header.Add("Content-Type", "application/json") defer func() {
req.Header.Add("X-Seed", common.Bytes2Hex(user.Seed())) if err := recover(); err != nil {
i.log.Error("worker paniced!", slog.Any("panic", err))
}
resp, err := http.DefaultClient.Do(req) doneCh <- struct{}{}
if err != nil { close(doneCh)
return fmt.Errorf("error fetch deploy salary contract. %w", err) }()
}
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 return nil
} }
type ListMultisigsParams struct { type ListMultisigsParams struct {
IDs uuid.UUIDs
OrganizationID uuid.UUID OrganizationID uuid.UUID
} }
@ -242,6 +470,7 @@ func (i *chainInteractor) ListMultisigs(
params ListMultisigsParams, params ListMultisigsParams,
) ([]models.Multisig, error) { ) ([]models.Multisig, error) {
multisigs, err := i.txRepository.ListMultisig(ctx, transactions.ListMultisigsParams{ multisigs, err := i.txRepository.ListMultisig(ctx, transactions.ListMultisigsParams{
IDs: params.IDs,
OrganizationID: params.OrganizationID, OrganizationID: params.OrganizationID,
}) })
if err != nil { if err != nil {

View File

@ -44,6 +44,7 @@ type ParticipantParams struct {
UsersOnly bool UsersOnly bool
ActiveOnly bool ActiveOnly bool
EmployeesOnly bool EmployeesOnly bool
OwnerOnly bool
} }
type ParticipantsParams struct { type ParticipantsParams struct {
@ -54,6 +55,7 @@ type ParticipantsParams struct {
UsersOnly bool UsersOnly bool
ActiveOnly bool ActiveOnly bool
EmployeesOnly bool EmployeesOnly bool
OwnerOnly bool
} }
type OrganizationsInteractor interface { type OrganizationsInteractor interface {

View File

@ -10,6 +10,7 @@ import (
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta" "github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
"github.com/emochka2007/block-accounting/internal/pkg/models" "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/interactors/organizations"
"github.com/emochka2007/block-accounting/internal/usecase/repository/transactions" "github.com/emochka2007/block-accounting/internal/usecase/repository/transactions"
"github.com/google/uuid" "github.com/google/uuid"
@ -59,20 +60,23 @@ type TransactionsInteractor interface {
} }
type transactionsInteractor struct { type transactionsInteractor struct {
log *slog.Logger log *slog.Logger
txRepo transactions.Repository txRepo transactions.Repository
orgInteractor organizations.OrganizationsInteractor orgInteractor organizations.OrganizationsInteractor
chainInteractor chain.ChainInteractor
} }
func NewTransactionsInteractor( func NewTransactionsInteractor(
log *slog.Logger, log *slog.Logger,
txRepo transactions.Repository, txRepo transactions.Repository,
orgInteractor organizations.OrganizationsInteractor, orgInteractor organizations.OrganizationsInteractor,
chainInteractor chain.ChainInteractor,
) TransactionsInteractor { ) TransactionsInteractor {
return &transactionsInteractor{ return &transactionsInteractor{
log: log, log: log,
txRepo: txRepo, txRepo: txRepo,
orgInteractor: orgInteractor, 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) return nil, fmt.Errorf("error confirm transaction. %w", err)
} }
// TODO confirm tx via chain-api
tx, err := i.txRepo.GetTransactions(ctx, transactions.GetTransactionsParams{ tx, err := i.txRepo.GetTransactions(ctx, transactions.GetTransactionsParams{
Ids: uuid.UUIDs{params.TxID}, Ids: uuid.UUIDs{params.TxID},
OrganizationId: params.OrganizationID, OrganizationId: params.OrganizationID,
@ -240,6 +246,8 @@ func (i *transactionsInteractor) Confirm(ctx context.Context, params ConfirmPara
return tx[0], nil return tx[0], nil
} }
// TODO Execute()
func (i *transactionsInteractor) Cancel(ctx context.Context, params CancelParams) (*models.Transaction, error) { func (i *transactionsInteractor) Cancel(ctx context.Context, params CancelParams) (*models.Transaction, error) {
user, err := ctxmeta.User(ctx) user, err := ctxmeta.User(ctx)
if err != nil { if err != nil {

View File

@ -54,6 +54,9 @@ type Repository interface {
AddMultisig(ctx context.Context, multisig models.Multisig) error AddMultisig(ctx context.Context, multisig models.Multisig) error
ListMultisig(ctx context.Context, params ListMultisigsParams) ([]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 { type repositorySQL struct {
@ -478,6 +481,12 @@ func (r *repositorySQL) ListMultisig(
"organization_id": params.OrganizationID, "organization_id": params.OrganizationID,
}).PlaceholderFormat(sq.Dollar) }).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) rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil { if err != nil {
return fmt.Errorf("error fetch multisigs from database. %w", err) return fmt.Errorf("error fetch multisigs from database. %w", err)
@ -543,6 +552,94 @@ func (r *repositorySQL) ListMultisig(
return msgs, nil 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 { type fetchOwnersParams struct {
MultisigID uuid.UUID MultisigID uuid.UUID
OrganizationID uuid.UUID OrganizationID uuid.UUID

View File

@ -99,7 +99,7 @@ create table if not exists transactions (
amount decimal default 0, amount decimal default 0,
to_addr bytea not null, to_addr bytea not null,
tx_index bytea default null, tx_index bigint default 0,
max_fee_allowed decimal default 0, max_fee_allowed decimal default 0,
deadline timestamp default null, 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 create index if not exists idx_multisig_confirmations_owners_owner_id
on multisig_confirmations (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 ( create table invites (
link_hash varchar(64) primary key, link_hash varchar(64) primary key,
organization_id uuid, organization_id uuid,
@ -200,4 +203,19 @@ create table invites (
created_at timestamp default current_timestamp, created_at timestamp default current_timestamp,
expired_at timestamp default null, expired_at timestamp default null,
used_at timestamp default null used_at timestamp default null
); );
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