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("x-seed", common.Bytes2Hex(user.Seed())) 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:] if pubKeyStr == "" { return nil, fmt.Errorf("error empty public key") } 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) } i.log.Debug( "not implemented", slog.String("org id", organizationID.String()), slog.Any("user", user), ) return nil }