package service import ( "context" "errors" "fmt" "regexp" "time" appconfig "github.com/D36u99er/bc-netts-energy/internal/config" "github.com/D36u99er/bc-netts-energy/internal/netts" "log/slog" ) var tronAddressPattern = regexp.MustCompile(`^T[1-9A-HJ-NP-Za-km-z]{33}$`) // EnsureEnergyRequest describes a request to guarantee sufficient energy. type EnsureEnergyRequest struct { FromAddress string ToAddress string Amount string CallbackURL string } // EnsureEnergyResponse summarises the orchestration result. type EnsureEnergyResponse struct { FromAddress string `json:"from_address"` ToAddress string `json:"to_address"` Amount string `json:"amount"` AddressAdded bool `json:"address_added"` RecommendedEnergy int `json:"recommended_energy"` EnergyNeeded int `json:"energy_needed"` CyclesBefore int `json:"cycles_before"` CyclesAfter int `json:"cycles_after"` CyclesPurchased int `json:"cycles_purchased,omitempty"` TotalCycles int `json:"total_cycles,omitempty"` OrderID string `json:"order_id,omitempty"` OrderStatus string `json:"order_status,omitempty"` TotalCost float64 `json:"total_cost,omitempty"` NextDelegation int64 `json:"next_delegation_time,omitempty"` Analysis netts.TransferDetails `json:"analysis"` OrderRaw *netts.OrderResult `json:"-"` Status *netts.AddressStatus `json:"-"` } // EnergyService coordinates Netts energy rentals. type NettsClient interface { AnalyzeUSDT(ctx context.Context, req netts.AnalyzeUSDTRequest) (*netts.AnalyzeUSDTData, error) GetAddressStatus(ctx context.Context, address string) (*netts.AddressStatus, error) AddHostAddress(ctx context.Context, address, callback string) (*netts.AddAddressResult, error) OrderCycles(ctx context.Context, address string, cycles int) (*netts.OrderResult, error) } type EnergyService struct { cfg appconfig.EnergyConfig nettsClient NettsClient logger *slog.Logger } // NewEnergyService instantiates an EnergyService. func NewEnergyService(cfg appconfig.EnergyConfig, client NettsClient, logger *slog.Logger) *EnergyService { return &EnergyService{ cfg: cfg, nettsClient: client, logger: logger, } } // EnsureEnergy ensures an address has sufficient energy and cycles. func (s *EnergyService) EnsureEnergy(ctx context.Context, req EnsureEnergyRequest) (*EnsureEnergyResponse, error) { if err := s.validateRequest(req); err != nil { return nil, err } amount := req.Amount if amount == "" { amount = s.cfg.DefaultAnalyzeValue } analysis, err := s.nettsClient.AnalyzeUSDT(ctx, netts.AnalyzeUSDTRequest{ Sender: req.FromAddress, Receiver: req.ToAddress, Amount: amount, }) if err != nil { return nil, fmt.Errorf("analyze transfer: %w", err) } transfer := analysis.TransferDetails energyNeeded := transfer.EnergyNeeded if energyNeeded == 0 { energyNeeded = transfer.RecommendedEnergy } s.logger.InfoContext(ctx, "analysis completed", "from", req.FromAddress, "to", req.ToAddress, "recommended_energy", transfer.RecommendedEnergy, "energy_needed", energyNeeded, ) status, addressAdded, err := s.ensureHostMode(ctx, req.FromAddress, req.CallbackURL) if err != nil { return nil, err } var ( cyclesBefore = 0 cyclesAfter = 0 order *netts.OrderResult ) if status != nil { cyclesBefore = status.CyclesRemaining if shouldOrderCycles(status, s.cfg.MinCycles) { targetCycles := s.cfg.TargetCycles if targetCycles < s.cfg.MinCycles { targetCycles = s.cfg.MinCycles } cyclesToBuy := targetCycles - safeCycleValue(status.CyclesRemaining) if cyclesToBuy > 0 { order, err = s.orderCycles(ctx, req.FromAddress, cyclesToBuy) if err != nil { return nil, err } if s.cfg.PostOrderWait.Duration() > 0 { select { case <-time.After(s.cfg.PostOrderWait.Duration()): case <-ctx.Done(): return nil, ctx.Err() } } status, _, err = s.ensureHostMode(ctx, req.FromAddress, req.CallbackURL) if err != nil { return nil, err } } } } if status != nil { cyclesAfter = status.CyclesRemaining } resp := &EnsureEnergyResponse{ FromAddress: req.FromAddress, ToAddress: req.ToAddress, Amount: amount, AddressAdded: addressAdded, RecommendedEnergy: transfer.RecommendedEnergy, EnergyNeeded: energyNeeded, CyclesBefore: cyclesBefore, CyclesAfter: cyclesAfter, Analysis: transfer, OrderRaw: order, Status: status, } if order != nil { resp.CyclesPurchased = order.CyclesPurchased resp.TotalCycles = order.TotalCycles resp.OrderID = order.OrderID resp.TotalCost = order.TotalCost resp.OrderStatus = order.Status resp.NextDelegation = order.NextDelegation } return resp, nil } func (s *EnergyService) ensureHostMode(ctx context.Context, address, callbackURL string) (*netts.AddressStatus, bool, error) { status, err := s.nettsClient.GetAddressStatus(ctx, address) if err == nil { return status, false, nil } if !errors.Is(err, netts.ErrAddressNotFound) { return nil, false, fmt.Errorf("get address status: %w", err) } if !s.cfg.AutoAddHost { return nil, false, fmt.Errorf("address %s not in host mode and autoAddHost disabled", address) } if _, err := s.nettsClient.AddHostAddress(ctx, address, callbackURL); err != nil { return nil, false, fmt.Errorf("add host address: %w", err) } s.logger.InfoContext(ctx, "address added to Host Mode", "address", address) status, err = s.nettsClient.GetAddressStatus(ctx, address) if err != nil { return nil, true, fmt.Errorf("get address status after add: %w", err) } return status, true, nil } func (s *EnergyService) orderCycles(ctx context.Context, address string, cycles int) (*netts.OrderResult, error) { if cycles <= 0 { return nil, nil } if cycles > 1000 { cycles = 1000 } order, err := s.nettsClient.OrderCycles(ctx, address, cycles) if err != nil { return nil, fmt.Errorf("order cycles: %w", err) } s.logger.InfoContext(ctx, "cycles ordered", "address", address, "cycles_purchased", order.CyclesPurchased, "total_cycles", order.TotalCycles, "order_id", order.OrderID, ) return order, nil } func (s *EnergyService) validateRequest(req EnsureEnergyRequest) error { if !tronAddressPattern.MatchString(req.FromAddress) { return fmt.Errorf("invalid from_address: %s", req.FromAddress) } if !tronAddressPattern.MatchString(req.ToAddress) { return fmt.Errorf("invalid to_address: %s", req.ToAddress) } return nil } func shouldOrderCycles(status *netts.AddressStatus, minCycles int) bool { if status == nil { return true } if status.CyclesRemaining < 0 { // Infinity mode (-1) requires no manual orders. return false } return status.CyclesRemaining < minCycles } func safeCycleValue(value int) int { if value < 0 { return 0 } return value }