block-accounting/backend/internal/usecase/interactors/chain/chain.go
2024-05-29 01:57:31 +03:00

532 lines
12 KiB
Go

package chain
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"time"
"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"
"github.com/google/uuid"
)
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)
PayrollDeploy(ctx context.Context, params PayrollDeployParams) error
ListPayrolls(ctx context.Context, params ListPayrollsParams) ([]models.Payroll, error)
}
type chainInteractor struct {
log *slog.Logger
config config.Config
txRepository transactions.Repository
}
func NewChainInteractor(
log *slog.Logger,
config config.Config,
txRepository transactions.Repository,
) ChainInteractor {
return &chainInteractor{
log: log,
config: config,
txRepository: txRepository,
}
}
type NewMultisigParams struct {
Title string
Owners []models.OrganizationParticipant
Confirmations int
}
type newMultisigChainResponse struct {
Address string `json:"address"`
}
func (i *chainInteractor) NewMultisig(ctx context.Context, params NewMultisigParams) error {
endpoint := i.config.ChainAPI.Host + "/multi-sig/deploy"
i.log.Debug(
"deploy multisig",
slog.String("endpoint", endpoint),
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())
}
requestBody, err := json.Marshal(map[string]any{
"owners": pks,
"confirmations": params.Confirmations,
})
if err != nil {
return fmt.Errorf("error marshal request body. %w", err)
}
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)
}
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(requestContext, http.MethodPost, endpoint, body)
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 send deploy multisig request",
slog.String("endpoint", endpoint),
slog.Any("params", params),
)
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
}
func (i *chainInteractor) PubKey(ctx context.Context, user *models.User) ([]byte, error) {
pubAddr := i.config.ChainAPI.Host + "/address-from-seed"
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)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, pubAddr, body)
if err != nil {
return nil, fmt.Errorf("error build request. %w", err)
}
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error fetch pub address. %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error read resp body. %w", err)
}
pubKeyStr := string(respBody)[2:]
return common.Hex2Bytes(pubKeyStr), nil
}
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() != 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": 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"
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
}
i.log.Debug(
"payroll deploy",
slog.Any("response", respObject),
)
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
}
func (i *chainInteractor) ListMultisigs(
ctx context.Context,
params ListMultisigsParams,
) ([]models.Multisig, error) {
multisigs, err := i.txRepository.ListMultisig(ctx, transactions.ListMultisigsParams{
IDs: params.IDs,
OrganizationID: params.OrganizationID,
})
if err != nil {
return nil, fmt.Errorf("error fetch multisigs. %w", err)
}
return multisigs, nil
}
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
}
type NewSalaryParams struct {
OrganizationID uuid.UUID
EmployeeID uuid.UUID
}
func (i *chainInteractor) NewSalary(
ctx context.Context,
params NewSalaryParams,
) 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)
}
return nil
}