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**
Feth txs
### Request body:

View File

@ -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,
)
}

View File

@ -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)

View File

@ -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) {

View File

@ -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"`
}

View File

@ -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"),
)
})
})
})

View File

@ -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
}

View File

@ -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,11 +93,59 @@ func (i *chainInteractor) NewMultisig(ctx context.Context, params NewMultisigPar
return fmt.Errorf("error fetch organization id from context. %w", err)
}
go func() {
pid := uuid.Must(uuid.NewV7()).String()
startTime := time.Now()
i.log.Info(
"new multisig worker started",
slog.String("pid", pid),
)
doneCh := make(chan struct{})
defer func() {
if err := recover(); err != nil {
i.log.Error("worker paniced!", slog.Any("panic", err))
}
doneCh <- struct{}{}
close(doneCh)
}()
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)),
)
}
}()
requestContext, cancel := context.WithTimeout(context.TODO(), time.Minute*15)
defer cancel()
body := bytes.NewBuffer(requestBody)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
req, err := http.NewRequestWithContext(requestContext, http.MethodPost, endpoint, body)
if err != nil {
return fmt.Errorf("error build request. %w", err)
i.log.Error(
"error build request",
logger.Err(err),
)
return
}
req.Header.Add("Content-Type", "application/json")
@ -108,24 +159,38 @@ func (i *chainInteractor) NewMultisig(ctx context.Context, params NewMultisigPar
slog.Any("params", params),
)
return fmt.Errorf("error build new multisig request. %w", err)
return
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("error read body. %w", err)
i.log.Error(
"error read body",
logger.Err(err),
)
return
}
respObject := new(newMultisigChainResponse)
if err := json.Unmarshal(raw, &respObject); err != nil {
return fmt.Errorf("error parse chain-api response body. %w", err)
i.log.Error(
"error parse chain-api response body",
logger.Err(err),
)
return
}
if respObject.Address == "" {
return fmt.Errorf("error multisig address is empty")
i.log.Error(
"error multisig address is empty",
)
return
}
multisigAddress := common.Hex2Bytes(respObject.Address[2:])
@ -151,9 +216,15 @@ func (i *chainInteractor) NewMultisig(ctx context.Context, params NewMultisigPar
slog.Any("multisig object", msg),
)
if err := i.txRepository.AddMultisig(ctx, msg); err != nil {
return fmt.Errorf("error add new multisig. %w", err)
if err := i.txRepository.AddMultisig(requestContext, msg); err != nil {
i.log.Error(
"error add new multisig",
logger.Err(err),
)
return
}
}()
return nil
}
@ -194,30 +265,131 @@ 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)
}
go func() {
pid := uuid.Must(uuid.NewV7()).String()
startTime := time.Now()
i.log.Info(
"new paroll worker started",
slog.String("pid", pid),
)
doneCh := make(chan struct{})
defer func() {
if err := recover(); err != nil {
i.log.Error("worker paniced!", slog.Any("panic", err))
}
doneCh <- struct{}{}
close(doneCh)
}()
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"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
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 {
return fmt.Errorf("error build request. %w", err)
i.log.Error(
"error build request",
logger.Err(fmt.Errorf("error build request. %w", err)),
)
return
}
req.Header.Add("Content-Type", "application/json")
@ -225,15 +397,71 @@ func (i *chainInteractor) SalaryDeploy(ctx context.Context, firtsAdmin models.Or
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("error fetch deploy salary contract. %w", err)
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 {

View File

@ -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 {

View File

@ -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"
@ -62,17 +63,20 @@ type transactionsInteractor struct {
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,
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 {

View File

@ -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

View File

@ -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,
@ -201,3 +204,18 @@ create table invites (
expired_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