block-accounting/backend/internal/usecase/interactors/chain/chain.go

609 lines
14 KiB
Go
Raw Permalink Normal View History

2024-05-24 17:44:24 +00:00
package chain
import (
2024-05-24 22:45:56 +00:00
"bytes"
2024-05-24 17:44:24 +00:00
"context"
2024-05-24 22:45:56 +00:00
"encoding/json"
"fmt"
2024-05-24 23:39:32 +00:00
"io"
2024-05-24 22:45:56 +00:00
"log/slog"
"net/http"
"time"
2024-05-24 17:44:24 +00:00
2024-06-14 17:47:58 +00:00
"github.com/emochka2007/block-accounting/internal/infrastructure/ethapi"
2024-06-03 21:54:17 +00:00
"github.com/emochka2007/block-accounting/internal/infrastructure/repository/transactions"
2024-05-24 22:45:56 +00:00
"github.com/emochka2007/block-accounting/internal/pkg/config"
2024-05-25 13:00:21 +00:00
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
2024-05-27 21:29:05 +00:00
"github.com/emochka2007/block-accounting/internal/pkg/logger"
2024-05-24 22:45:56 +00:00
"github.com/emochka2007/block-accounting/internal/pkg/models"
2024-06-03 21:54:17 +00:00
"github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations"
2024-05-24 22:45:56 +00:00
"github.com/ethereum/go-ethereum/common"
"github.com/google/uuid"
2024-05-24 17:44:24 +00:00
)
type ChainInteractor interface {
2024-05-27 21:29:05 +00:00
PubKey(ctx context.Context, user *models.User) ([]byte, error)
2024-05-24 22:45:56 +00:00
NewMultisig(ctx context.Context, params NewMultisigParams) error
2024-05-26 18:20:14 +00:00
ListMultisigs(ctx context.Context, params ListMultisigsParams) ([]models.Multisig, error)
2024-05-27 21:29:05 +00:00
PayrollDeploy(ctx context.Context, params PayrollDeployParams) error
2024-05-28 20:14:15 +00:00
ListPayrolls(ctx context.Context, params ListPayrollsParams) ([]models.Payroll, error)
2024-06-03 21:54:17 +00:00
SetSalary(ctx context.Context, params SetSalaryParams) error
2024-05-24 17:44:24 +00:00
}
type chainInteractor struct {
2024-06-03 21:54:17 +00:00
log *slog.Logger
config config.Config
txRepository transactions.Repository
organizationsInteractor organizations.OrganizationsInteractor
2024-06-14 17:47:58 +00:00
client ethapi.EthAPIClient
2024-05-24 17:44:24 +00:00
}
2024-05-24 22:45:56 +00:00
func NewChainInteractor(
log *slog.Logger,
config config.Config,
txRepository transactions.Repository,
2024-06-03 21:54:17 +00:00
organizationsInteractor organizations.OrganizationsInteractor,
2024-06-14 17:47:58 +00:00
client ethapi.EthAPIClient,
2024-05-24 22:45:56 +00:00
) ChainInteractor {
return &chainInteractor{
2024-06-03 21:54:17 +00:00
log: log,
config: config,
txRepository: txRepository,
organizationsInteractor: organizationsInteractor,
2024-05-24 22:45:56 +00:00
}
}
2024-05-24 17:44:24 +00:00
type NewMultisigParams struct {
Title string
Owners []models.OrganizationParticipant
2024-05-24 22:45:56 +00:00
Confirmations int
2024-05-24 17:44:24 +00:00
}
2024-05-24 22:45:56 +00:00
func (i *chainInteractor) NewMultisig(ctx context.Context, params NewMultisigParams) error {
2024-05-25 13:00:21 +00:00
endpoint := i.config.ChainAPI.Host + "/multi-sig/deploy"
2024-05-24 22:45:56 +00:00
i.log.Debug(
"deploy multisig",
2024-05-25 13:00:21 +00:00
slog.String("endpoint", endpoint),
2024-05-24 22:45:56 +00:00
slog.Any("params", params),
)
pks := make([]string, len(params.Owners))
for i, owner := range params.Owners {
if owner.GetUser() == nil {
return fmt.Errorf("error invalis owners set")
}
pks[i] = "0x" + common.Bytes2Hex(owner.GetUser().PublicKey())
}
2024-05-25 13:00:21 +00:00
user, err := ctxmeta.User(ctx)
if err != nil {
return fmt.Errorf("error fetch user from context. %w", err)
}
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return fmt.Errorf("error fetch organization id from context. %w", err)
}
2024-06-14 17:47:58 +00:00
go func() { // TODO remove this subroutine shit and replace it with worker queue tasks
2024-05-27 21:29:05 +00:00
pid := uuid.Must(uuid.NewV7()).String()
startTime := time.Now()
2024-05-24 22:45:56 +00:00
2024-05-27 21:29:05 +00:00
i.log.Info(
"new multisig worker started",
slog.String("pid", pid),
)
2024-05-24 22:45:56 +00:00
2024-06-03 21:54:17 +00:00
doneCh := make(chan struct{}, 1)
2024-05-27 21:29:05 +00:00
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()
2024-06-14 17:47:58 +00:00
requestContext = ctxmeta.UserContext(requestContext, user)
2024-05-27 21:29:05 +00:00
2024-06-14 17:47:58 +00:00
address, err := i.client.DeployMultisig(requestContext, pks, params.Confirmations)
2024-05-27 21:29:05 +00:00
if err != nil {
i.log.Error(
"error send deploy multisig request",
slog.String("endpoint", endpoint),
slog.Any("params", params),
)
2024-05-24 22:45:56 +00:00
2024-05-27 21:29:05 +00:00
return
}
2024-05-24 22:45:56 +00:00
2024-06-14 17:47:58 +00:00
multisigAddress := common.Hex2Bytes(address[2:])
2024-05-27 21:29:05 +00:00
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,
}
if err := i.txRepository.AddMultisig(requestContext, msg); err != nil {
i.log.Error(
"error add new multisig",
logger.Err(err),
)
return
}
}()
2024-05-27 18:08:19 +00:00
2024-05-25 13:00:21 +00:00
return nil
2024-05-24 22:45:56 +00:00
}
2024-05-24 23:39:32 +00:00
func (i *chainInteractor) PubKey(ctx context.Context, user *models.User) ([]byte, error) {
pubAddr := i.config.ChainAPI.Host + "/address-from-seed"
2024-05-24 22:45:56 +00:00
2024-05-24 23:39:32 +00:00
requestBody, err := json.Marshal(map[string]any{
"seedPhrase": user.Mnemonic,
})
if err != nil {
return nil, fmt.Errorf("error marshal request body. %w", err)
}
body := bytes.NewBuffer(requestBody)
2024-05-25 13:00:21 +00:00
req, err := http.NewRequestWithContext(ctx, http.MethodPost, pubAddr, body)
if err != nil {
return nil, fmt.Errorf("error build request. %w", err)
}
2024-05-24 23:39:32 +00:00
2024-05-29 21:18:29 +00:00
req.Header.Add("X-Seed", common.Bytes2Hex(user.Seed()))
2024-05-25 13:00:21 +00:00
req.Header.Add("Content-Type", "application/json")
2024-05-24 23:39:32 +00:00
2024-06-03 21:54:17 +00:00
// TODO replace http.DefaultClient with custom ChainAPi client from infrastructure/ethapi mod
2024-05-25 13:00:21 +00:00
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error fetch pub address. %w", err)
}
2024-05-24 23:39:32 +00:00
2024-05-25 13:00:21 +00:00
defer resp.Body.Close()
2024-05-24 23:39:32 +00:00
2024-05-25 13:00:21 +00:00
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error read resp body. %w", err)
}
2024-05-24 23:39:32 +00:00
2024-05-25 13:00:21 +00:00
pubKeyStr := string(respBody)[2:]
2024-05-24 17:44:24 +00:00
2024-05-28 22:56:24 +00:00
if pubKeyStr == "" {
2024-06-14 17:47:58 +00:00
return nil, fmt.Errorf("error empty public key...")
2024-05-28 22:56:24 +00:00
}
2024-05-25 13:00:21 +00:00
return common.Hex2Bytes(pubKeyStr), nil
}
2024-05-27 21:29:05 +00:00
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 {
2024-05-25 13:00:21 +00:00
user, err := ctxmeta.User(ctx)
if err != nil {
return fmt.Errorf("error fetch user from context. %w", err)
2024-05-24 22:45:56 +00:00
}
2024-05-25 13:00:21 +00:00
2024-05-27 21:29:05 +00:00
if user.Id() != params.FirstAdmin.Id() || params.FirstAdmin.GetUser() == nil {
2024-05-25 13:00:21 +00:00
return fmt.Errorf("error unauthorized access")
}
2024-05-27 21:29:05 +00:00
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},
2024-05-25 13:00:21 +00:00
})
if err != nil {
2024-05-27 21:29:05 +00:00
return fmt.Errorf("error fetch multisigs by id. %w", err)
2024-05-25 13:00:21 +00:00
}
2024-05-27 21:29:05 +00:00
if len(multisigs) == 0 {
return fmt.Errorf("error empty multisigs set")
}
2024-05-25 13:00:21 +00:00
2024-05-27 21:29:05 +00:00
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())),
)
2024-05-25 13:00:21 +00:00
2024-05-27 21:29:05 +00:00
maddr := common.Bytes2Hex(multisigs[0].Address)
if maddr == "" {
return fmt.Errorf("empty multisig address")
2024-05-25 13:00:21 +00:00
}
2024-05-27 21:29:05 +00:00
if maddr[0] != 0 && maddr[1] != 'x' {
maddr = "0x" + maddr
}
2024-05-25 13:00:21 +00:00
2024-05-27 21:29:05 +00:00
requestBody, err := json.Marshal(map[string]any{
"authorizedWallet": maddr,
})
2024-05-25 13:00:21 +00:00
if err != nil {
2024-05-27 21:29:05 +00:00
return fmt.Errorf("error marshal request body. %w", err)
2024-05-25 13:00:21 +00:00
}
2024-06-03 21:54:17 +00:00
go func() { // TODO remove this subroutine shit and replace it with worker pools
2024-05-27 21:29:05 +00:00
pid := uuid.Must(uuid.NewV7()).String()
startTime := time.Now()
i.log.Info(
"new paroll worker started",
slog.String("pid", pid),
)
2024-06-03 21:54:17 +00:00
doneCh := make(chan struct{}, 1)
2024-05-27 21:29:05 +00:00
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(
2024-06-03 21:54:17 +00:00
"new payroll worker done",
2024-05-27 21:29:05 +00:00
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")
2024-05-29 21:18:29 +00:00
req.Header.Add("X-Seed", common.Bytes2Hex(user.Seed()))
2024-05-27 21:29:05 +00:00
2024-06-03 21:54:17 +00:00
// TODO replace http.DefaultClient with custom ChainAPi client from infrastructure/ethapi mod
2024-05-27 21:29:05 +00:00
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
}
2024-05-28 20:14:15 +00:00
i.log.Debug(
"payroll deploy",
slog.Any("response", respObject),
)
2024-05-27 21:29:05 +00:00
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
}
}()
2024-05-25 13:00:21 +00:00
return nil
2024-05-24 17:44:24 +00:00
}
2024-05-25 13:00:21 +00:00
2024-05-26 18:20:14 +00:00
type ListMultisigsParams struct {
2024-05-27 21:29:05 +00:00
IDs uuid.UUIDs
2024-05-26 18:20:14 +00:00
OrganizationID uuid.UUID
}
func (i *chainInteractor) ListMultisigs(
ctx context.Context,
params ListMultisigsParams,
) ([]models.Multisig, error) {
multisigs, err := i.txRepository.ListMultisig(ctx, transactions.ListMultisigsParams{
2024-05-27 21:29:05 +00:00
IDs: params.IDs,
2024-05-26 18:20:14 +00:00
OrganizationID: params.OrganizationID,
})
if err != nil {
return nil, fmt.Errorf("error fetch multisigs. %w", err)
}
return multisigs, nil
}
2024-05-28 20:14:15 +00:00
type ListPayrollsParams struct {
IDs []uuid.UUID
Limit int
OrganizationID uuid.UUID
}
func (i *chainInteractor) ListPayrolls(
ctx context.Context,
params ListPayrollsParams,
) ([]models.Payroll, error) {
payrolls, err := i.txRepository.ListPayrolls(ctx, transactions.ListPayrollsParams{
IDs: params.IDs,
Limit: int64(params.Limit),
OrganizationID: params.OrganizationID,
})
if err != nil {
return nil, fmt.Errorf("error fetch payrolls from repository. %w", err)
}
return payrolls, nil
}
2024-06-03 21:54:17 +00:00
type SetSalaryParams struct {
PayrollID uuid.UUID
EmployeeID uuid.UUID
Salary float64
2024-05-28 20:14:15 +00:00
}
2024-06-03 21:54:17 +00:00
func (i *chainInteractor) SetSalary(
2024-05-28 20:14:15 +00:00
ctx context.Context,
2024-06-03 21:54:17 +00:00
params SetSalaryParams,
2024-05-28 20:14:15 +00:00
) error {
user, err := ctxmeta.User(ctx)
if err != nil {
return fmt.Errorf("error fetch user from context. %w", err)
}
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return fmt.Errorf("error fetch organization id from context. %w", err)
}
2024-05-28 20:16:37 +00:00
i.log.Debug(
2024-06-03 21:54:17 +00:00
"SetSalary",
2024-05-28 20:16:37 +00:00
slog.String("org id", organizationID.String()),
slog.Any("user", user),
)
2024-06-03 21:54:17 +00:00
payrolls, err := i.ListPayrolls(ctx, ListPayrollsParams{
IDs: []uuid.UUID{params.PayrollID},
OrganizationID: organizationID,
})
if err != nil {
return fmt.Errorf("error fetch payroll. %w", err)
}
if len(payrolls) == 0 {
return fmt.Errorf("error payroll not found. %w", err)
}
payroll := payrolls[0]
multisigs, err := i.ListMultisigs(ctx, ListMultisigsParams{
IDs: uuid.UUIDs{payroll.MultisigID},
OrganizationID: organizationID,
})
if err != nil {
return fmt.Errorf("error fetch multisig. %w", err)
}
if len(multisigs) == 0 {
return fmt.Errorf("error multisig not found. %w", err)
}
multisig := multisigs[0]
employee, err := i.organizationsInteractor.Participant(ctx, organizations.ParticipantParams{
ID: params.EmployeeID,
OrganizationID: organizationID,
EmployeesOnly: true,
})
if err != nil {
return fmt.Errorf("error fetch employee from repository. %w", err)
}
if employee.GetEmployee() == nil {
return fmt.Errorf("error employee is nil")
}
go func() {
defer func() {
if err := recover(); err != nil {
i.log.Error("worker paniced!", slog.Any("panic", err))
}
// doneCh <- struct{}{}
// close(doneCh)
}()
ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Minute)
defer cancel()
maddr := common.Bytes2Hex(multisig.Address)
if maddr[0] != 0 && maddr[1] != 'x' {
maddr = "0x" + maddr
}
caddr := common.Bytes2Hex(payroll.Address)
if caddr[0] != 0 && caddr[1] != 'x' {
caddr = "0x" + caddr
}
eaddr := common.Bytes2Hex(employee.GetEmployee().WalletAddress)
if caddr[0] != 0 && caddr[1] != 'x' {
caddr = "0x" + caddr
}
bodyMap := map[string]any{
"multiSigWallet": maddr,
"contractAddress": caddr,
"employeeAddress": eaddr,
"salary": params.Salary,
}
bodyRaw, err := json.Marshal(&bodyMap)
if err != nil {
i.log.Error(
"error marshal request body",
logger.Err(err),
)
return
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
i.config.ChainAPI.Host+"/salaries/set-salary",
bytes.NewBuffer(bodyRaw),
)
if err != nil {
i.log.Error(
"error build request",
logger.Err(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 do request",
logger.Err(err),
)
return
}
defer resp.Body.Close()
// todo parse body
}()
2024-05-28 20:14:15 +00:00
return nil
}