package alchemy import ( "context" "fmt" "gitea.pena/PenaSide/treasurer/internal/errors" "gitea.pena/PenaSide/treasurer/internal/models" "gitea.pena/PenaSide/treasurer/internal/models/alchemy" "gitea.pena/PenaSide/treasurer/internal/repository" "github.com/gofiber/fiber/v2" "github.com/google/uuid" "go.uber.org/zap" "strconv" "strings" "time" ) const ProviderName = "alchemy" type Config struct { WalletAddress string `json:"walletAddress"` } type Provider struct { repository *repository.PaymentRepository logger *zap.Logger config *Config } type Deps struct { Repository *repository.PaymentRepository Logger *zap.Logger Config *Config } func New(deps Deps) *Provider { return &Provider{ logger: deps.Logger, config: deps.Config, repository: deps.Repository, } } func (p *Provider) GetName() string { return ProviderName } func (p *Provider) GetSupportedPaymentMethods() []models.PaymentType { return []models.PaymentType{models.PaymentTypeAlchemy} } func (p *Provider) CreateInvoice(ctx context.Context, req map[string]string) (string, errors.Error) { amountStr := req["cryptoAmount"] fromWallet := req["fromWalletAddress"] if amountStr == "" || fromWallet == "" { p.logger.Error("amount or fromWallet address is empty", zap.String("fromWalletAddress", fromWallet), zap.String("cryptoAmount", amountStr)) return "", errors.NewWithMessage("cryptoAmount and fromWalletAddress required", errors.ErrInvalidArgs) } cryptoAmount, err := strconv.ParseFloat(amountStr, 64) if err != nil { p.logger.Error("failed to parse cryptoAmount from wallet address", zap.Error(err)) return "", errors.NewWithMessage("invalid cryptoAmount", errors.ErrInvalidArgs) } now := time.Now() payment := &models.Payment{ PaymentID: uuid.NewString(), UserID: req["user_id"], ClientIP: req["client_ip"], Currency: req["currency"], Type: models.PaymentTypeAlchemy, Status: models.PaymentStatusWaiting, CreatedAt: now, UpdatedAt: now, ToWalletAddress: p.config.WalletAddress, FromWalletAddress: fromWallet, CryptoAmount: cryptoAmount, } var callbackHosts []string if val, ok := req["callback_host_grpc"]; ok && val != "" { callbackHosts = strings.Split(val, ",") } payment.CallbackHostGRPC = callbackHosts _, err = p.repository.Insert(ctx, payment) if err != nil { p.logger.Error("failed to insert payment into database", zap.Error(err)) return "", errors.NewWithMessage(fmt.Sprintf("failed to insert payment into database: %v", err), errors.ErrInternalError) } return p.config.WalletAddress, nil } func (p *Provider) RegisterWebhookHandlers(router fiber.Router) { router.Post("/webhook/alchemy", p.handleWebhook) } func (p *Provider) handleWebhook(ctx *fiber.Ctx) error { var payload alchemy.AlchemyAddressActivityWebhook if err := ctx.BodyParser(&payload); err != nil { return ctx.Status(fiber.StatusBadRequest).SendString(fmt.Sprintf("failed to parse Alchemy webhook: %s", err.Error())) } for _, act := range payload.Event.Activity { if act.ToAddress != p.config.WalletAddress { continue } // todo нужно подумать как сделать так если сумма оплаты оказалась чуть больше ожидаемой... payment, err := p.repository.FindByWalletsAndAmount(ctx.Context(), act.ToAddress, act.FromAddress, act.Value) if err != nil { if err.Type() == errors.ErrNotFound { return ctx.Status(fiber.StatusNotFound).SendString(fmt.Sprintf("payment not found: %s", err.Error())) } return ctx.Status(fiber.StatusInternalServerError).SendString(fmt.Sprintf("internal error while searching payment: %s", err.Error())) } _, err = p.repository.SetPaymentStatus(ctx.Context(), payment.PaymentID, models.PaymentStatusSuccessfully) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(fmt.Sprintf("failed to set payment complete: %s", err.Error())) } } return ctx.SendStatus(fiber.StatusOK) }