Ctrl K
ComunidadGuías…

internal/domain/stars.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package domain

import (
	"encoding/base64"
	"errors"
	"fmt"
	"strconv"
)

// Stars local ledger domain model (no TL types, mirroring the boost.go style). This implementation is a local ledger,
// not a real payment: the balance is integer Stars (Nanos is always 0 online), and debits are atomic and never go negative.

// StarsBalance is an account's current available Stars balance.
type StarsBalance struct {
	UserID  int64
	Balance int64 // currently spendable Stars, always >= 0
	Granted bool  // whether the starting grant has been applied (idempotency guard for the lazy first-read grant)
}

// StarsPurchaseKind identifies the balance owner affected by a fiat Stars
// checkout. It is persisted with the form so a client cannot reinterpret a
// self top-up as a friend gift (or vice versa) when submitting the form.
type StarsPurchaseKind string

const (
	StarsPurchaseTopup    StarsPurchaseKind = "topup"
	StarsPurchaseGift     StarsPurchaseKind = "gift"
	StarsPurchaseGiveaway StarsPurchaseKind = "giveaway"
	// StarsPurchaseBotInvoice is a settled XTR invoice whose proceeds belong to
	// a bot's own revenue wallet rather than to the payer's personal balance.
	StarsPurchaseBotInvoice StarsPurchaseKind = "bot_invoice"
)

func (k StarsPurchaseKind) Valid() bool {
	return k == StarsPurchaseTopup || k == StarsPurchaseGift || k == StarsPurchaseGiveaway ||
		k == StarsPurchaseBotInvoice
}

// BotStarsCredit is one settled XTR invoice that credits a bot's own revenue
// wallet. InvoiceKey makes the credit idempotent across retried
// payments.sendPaymentForm calls: a replay returns the stored balance instead
// of minting a second credit. Amount must be positive; refunds carry their own
// key and a negative Amount.
type BotStarsCredit struct {
	BotUserID   int64
	PayerUserID int64
	Amount      int64
	Reason      StarsTransactionReason
	InvoiceKey  string
	Date        int
}

// Valid reports whether the credit describes a well-formed wallet mutation.
func (c BotStarsCredit) Valid() bool {
	switch c.Reason {
	case StarsReasonBotInvoice, StarsReasonBotRefund:
	default:
		return false
	}
	return c.BotUserID > 0 && c.PayerUserID > 0 && c.Date > 0 &&
		c.Amount != 0 && c.InvoiceKey != "" && len(c.InvoiceKey) <= 128
}

// BotStarsPayment is the durable receipt of one settled invoice credit. It
// records the post-transaction balance so a duplicate settlement can be
// answered without re-reading the mutable balance row.
type BotStarsPayment struct {
	InvoiceKey   string
	BotUserID    int64
	PayerUserID  int64
	Amount       int64
	BalanceAfter int64
	SettledAt    int
}

// BotStarsRevenuePoint is one UTC day of bot wallet activity. DayStart is the
// Unix second at 00:00 UTC, which the RPC layer multiplies by 1000 because the
// official chart renderer expects millisecond x values.
//
// Amount is the day's inbound sum and Spent the day's outbound sum. Spent is
// deliberately not called "withdrawn": Telegram uses "withdrawal" for a
// Fragment cash-out, which telesrv does not support for a bot wallet at all -
// withdrawal_enabled stays false - so that word would promise a payout that
// does not exist.
type BotStarsRevenuePoint struct {
	DayStart int
	Amount   int64
	Spent    int64
}

// Valid reports whether the point can be rendered on the revenue chart.
func (p BotStarsRevenuePoint) Valid() bool {
	return p.DayStart > 0 && p.Amount >= 0 && p.Spent >= 0
}

// StarsGiveawayPurchase is the complete immutable purpose behind one direct
// fiat Stars giveaway checkout. The launch purchase persists this shape; the
// eventual winner draw is a separate lifecycle transition.
type StarsGiveawayPurchase struct {
	BoostPeer          Peer     `json:"boost_peer"`
	AdditionalPeers    []Peer   `json:"additional_peers,omitempty"`
	CountriesISO2      []string `json:"countries_iso2,omitempty"`
	PrizeDescription   string   `json:"prize_description,omitempty"`
	RandomID           int64    `json:"random_id"`
	UntilDate          int      `json:"until_date"`
	Users              int      `json:"users"`
	PerUserStars       int64    `json:"per_user_stars"`
	YearlyBoosts       int      `json:"yearly_boosts"`
	OnlyNewSubscribers bool     `json:"only_new_subscribers,omitempty"`
	WinnersAreVisible  bool     `json:"winners_are_visible,omitempty"`
}

// StarsPurchaseForm binds one short-lived fiat Stars checkout to its
// authenticated buyer, purpose and exact server-advertised package. Recipient
// is zero for a self top-up and mandatory for a friend gift.
type StarsPurchaseForm struct {
	FormID           int64
	Kind             StarsPurchaseKind
	BuyerUserID      int64
	RecipientUserID  int64
	SpendPurposePeer Peer
	Giveaway         *StarsGiveawayPurchase
	Stars            int64
	Currency         string
	Amount           int64
	IssuedAt         int
	ExpiresAt        int
}

// StarsPurchaseRequest is the immutable settlement command carried by
// inputInvoiceStars. Android and TDesktop sendPaymentForm both resolve to this
// command after the ordinary fiat checkout has produced provider credentials.
type StarsPurchaseRequest struct {
	StarsPurchaseForm
	Date            int
	OriginAuthKeyID [8]byte
	OriginSessionID int64
}

// StarsPurchaseResult is the atomically committed credit and, for a friend
// gift, bilateral service-message receipt. Duplicate means an exact form replay.
type StarsPurchaseResult struct {
	Balance       StarsBalance
	Send          SendPrivateTextResult
	ChannelSend   SendChannelMessageResult
	TransactionID string
	Duplicate     bool
}

// StarsGiveawayInfo is the viewer-specific state of one durable launch card.
// Winner selection/results are intentionally outside the purchase aggregate.
type StarsGiveawayInfo struct {
	StartDate             int
	Participating         bool
	PreparingResults      bool
	JoinedTooEarlyDate    int
	AdminDisallowedChatID int64
	DisallowedCountry     string
}

// StarsTransactionReason marks the semantics of a ledger entry (projected to tg.StarsTransaction's flags/title).
type StarsTransactionReason string

const StarsReasonTransfer StarsTransactionReason = "transfer"

const (
	StarsReasonGrant           StarsTransactionReason = "grant"        // automatic starting-balance grant
	StarsReasonTopup           StarsTransactionReason = "topup"        // top-up (minted locally)
	StarsReasonReaction        StarsTransactionReason = "reaction"     // paid reaction spend
	StarsReasonGift            StarsTransactionReason = "gift"         // star gift spend/receive
	StarsReasonGiftUpgrade     StarsTransactionReason = "gift_upgrade" // upgrade a regular gift into a unique gift
	StarsReasonGiftTransfer    StarsTransactionReason = "gift_transfer"
	StarsReasonGiftResale      StarsTransactionReason = "gift_resale"
	StarsReasonGiftOffer       StarsTransactionReason = "gift_offer"
	StarsReasonGiftAuction     StarsTransactionReason = "gift_auction"
	StarsReasonGiftPrepaid     StarsTransactionReason = "gift_prepaid_upgrade"
	StarsReasonGiftDrop        StarsTransactionReason = "gift_drop_original_details"
	StarsReasonPaidMedia       StarsTransactionReason = "paid_media"   // paid media unlock
	StarsReasonPaidMessage     StarsTransactionReason = "paid_message" // channel Direct Message spend
	StarsReasonSuggestedPost   StarsTransactionReason = "suggested_post"
	StarsReasonPremium         StarsTransactionReason = "premium"
	StarsReasonWithdrawal      StarsTransactionReason = "withdrawal"
	StarsReasonAdjust          StarsTransactionReason = "adjust"           // fallback/manual adjustment
	StarsReasonDonationDeposit StarsTransactionReason = "donation_deposit" // crypto deposit credited after on-chain confirmation
	StarsReasonBotInvoice      StarsTransactionReason = "bot_invoice"      // settle an XTR invoice, crediting the bot wallet
	StarsReasonBotSpend        StarsTransactionReason = "bot_spend"        // bot wallet spend
	StarsReasonBotRefund       StarsTransactionReason = "bot_refund"       // invoice refund, debiting the bot wallet
)

// StarsTransaction is one ledger entry. amount is signed: credit > 0 (including refund/receive), debit < 0.
type StarsTransaction struct {
	ID              int64 // monotonic ledger id (keyset cursor)
	UserID          int64 // ledger owner
	Peer            Peer  // counterparty (a zero Peer when there is none, e.g. grant/topup)
	Amount          int64 // signed amount
	Date            int   // Unix seconds
	Reason          StarsTransactionReason
	Title           string // optional, projected to tg.StarsTransaction.Title
	Description     string // optional, projected to tg.StarsTransaction.Description
	PaymentID       int64
	RecipientUserID int64
	PremiumMonths   int
}

// IsCredit reports whether the entry is a credit, projected to tg.StarsTransaction.Refund.
func (t StarsTransaction) IsCredit() bool { return t.Amount > 0 }

// StarsTransactionDirection scopes one payments.getStarsTransactions view.
// The zero value intentionally means the combined inbound/outbound history.
type StarsTransactionDirection uint8

const (
	StarsTransactionDirectionAll StarsTransactionDirection = iota
	StarsTransactionDirectionIncoming
	StarsTransactionDirectionOutgoing
)

func (d StarsTransactionDirection) Valid() bool {
	return d <= StarsTransactionDirectionOutgoing
}

func (d StarsTransactionDirection) IncludesAmount(amount int64) bool {
	switch d {
	case StarsTransactionDirectionAll:
		return true
	case StarsTransactionDirectionIncoming:
		return amount > 0
	case StarsTransactionDirectionOutgoing:
		return amount < 0
	default:
		return false
	}
}

// StarsTransactionQuery keeps direction, ordering and the opaque keyset cursor
// together so filtering is applied before LIMIT in every ledger backend.
type StarsTransactionQuery struct {
	Offset    string
	Limit     int
	Direction StarsTransactionDirection
	Ascending bool
}

// NormalizeStarsTransactionQuery preserves the existing bounded limit/offset
// behavior while rejecting impossible internal direction values.
func NormalizeStarsTransactionQuery(query StarsTransactionQuery) (StarsTransactionQuery, error) {
	if !query.Direction.Valid() {
		return StarsTransactionQuery{}, ErrStarsTransactionQueryInvalid
	}
	if len(query.Offset) > MaxStarsTransactionsOffsetBytes {
		query.Offset = ""
	}
	if query.Limit <= 0 || query.Limit > MaxStarsTransactionsLimit {
		query.Limit = MaxStarsTransactionsLimit
	}
	return query, nil
}

// StarsTransactionPage is one page of ledger entries + current balance + pagination cursor + the counterparty user enrichment set.
type StarsTransactionPage struct {
	Balance      int64
	Transactions []StarsTransaction
	NextOffset   string // empty means no more pages (DrKLO stops paging on it; do not give a non-empty value on the last page)
	Users        []User // counterparty users mentioned in History, for tg Users enrichment
}

// TonUSDRate is the fixed local-ledger USD reference rate, not a live market quote.
const TonUSDRate = 1.3448279670684584

// TonTransaction is an entry in telesrv's internal nanoton ledger. It models
// the Telegram TON-denominated gift UI without contacting a wallet, Fragment,
// a TON node, or any blockchain service.
type TonTransaction struct {
	Comment     string
	ID          int64
	UserID      int64
	Peer        Peer
	GiftID      int64
	Amount      int64 // signed nanoton amount
	Date        int
	Reason      StarsTransactionReason
	Title       string
	Description string
}

type TonTransactionPage struct {
	Balance      int64
	Transactions []TonTransaction
	NextOffset   string
	Users        []User
}

// Stars ledger boundary constants.
const (
	// DefaultStarsStartingGrant is the starting Stars balance of the lazy first-read grant (for local testing).
	DefaultStarsStartingGrant = 1000
	// MaxStarsTransactionsLimit is the per-page limit of getStarsTransactions.
	MaxStarsTransactionsLimit = 100
	// MaxStarsTransactionsOffsetBytes is the keyset cursor string length limit.
	MaxStarsTransactionsOffsetBytes = 64
)

// Stars ledger sentinel errors (the rpc layer matches via errors.Is and maps to tgerr, mirroring ErrPremiumRequired).
var (
	// ErrStarsInsufficient means the balance is insufficient to complete the debit (maps to BALANCE_TOO_LOW).
	ErrStarsInsufficient = errors.New("stars: insufficient balance")
	// ErrStarsInvalidAmount means the amount is invalid (<=0).
	ErrStarsInvalidAmount = errors.New("stars: invalid amount")
	// ErrStarsTransactionQueryInvalid means an impossible transaction direction was constructed internally.
	ErrStarsTransactionQueryInvalid = errors.New("stars: invalid transaction query")
	// ErrStarsPurchaseFormInvalid covers a missing/cross-account/mutated form.
	ErrStarsPurchaseFormInvalid = errors.New("stars: purchase form invalid")
	// ErrStarsPurchaseFormExpired is returned before any settlement write.
	ErrStarsPurchaseFormExpired = errors.New("stars: purchase form expired")
	// ErrStarsGiftUnavailable covers a recipient that cannot receive the gift.
	ErrStarsGiftUnavailable = errors.New("stars: gift unavailable")
	// ErrStarsAirdropUnsupported means the underlying store does not support bulk credit (e.g. the in-memory backend).
	ErrStarsAirdropUnsupported = errors.New("stars: airdrop unsupported")
)

// StarsPaymentRequiredError reports the minimum paid-message authorization the
// sender must include in allow_paid_stars. The authorization is a ceiling; the
// ledger debits only the channel's current configured price.
type StarsPaymentRequiredError struct {
	Stars int64
}

func (e *StarsPaymentRequiredError) Error() string {
	return fmt.Sprintf("stars: allow payment required: %d", e.Stars)
}

// EncodeStarsCursor encodes the keyset cursor (the last entry id) into a client-opaque string.
func EncodeStarsCursor(id int64) string {
	return base64.RawURLEncoding.EncodeToString([]byte(strconv.FormatInt(id, 10)))
}

// DecodeStarsCursor reverses EncodeStarsCursor; it returns ok=false when it cannot parse (including an empty string),
// and the caller should start from the first page (the client only echoes back cursors we gave it; malformed is only a fallback).
func DecodeStarsCursor(s string) (int64, bool) {
	if s == "" {
		return 0, false
	}
	raw, err := base64.RawURLEncoding.DecodeString(s)
	if err != nil {
		return 0, false
	}
	id, err := strconv.ParseInt(string(raw), 10, 64)
	if err != nil || id <= 0 {
		return 0, false
	}
	return id, true
}

var ErrWalletTransferStartingGrant = errors.New("wallet transfers require a zero starting grant")
var ErrWalletTransferRecipientBlocked = errors.New("wallet transfer recipient blocked sender")

// WalletGramTransferRequest describes one gram transfer between two accounts.
//
// The request is the whole movement: who sends, who receives, how much, and
// what the note says. The address is the recipient's wallet address, which
// the caller knows from the wallet and the ledger does not.
type WalletGramTransferRequest struct {
	// AuthorizationHash identifies the one-use SRP grant; no secret is persisted.
	AuthorizationHash [32]byte
	// FromUserID is the account the money leaves.
	FromUserID int64
	// ToUserID is the account the money arrives at.
	ToUserID int64
	// AmountNanoton is the amount, in nanoton.
	AmountNanoton int64
	// Reason is the ledger reason of the movement.
	Reason StarsTransactionReason
	// PeerAddress is the recipient's wallet address, shown in the service
	// message. It is empty when the caller does not know it.
	PeerAddress string
	// Comment is the note the sender wrote on the transfer.
	Comment string
	// Date is when the movement happened.
	Date int
	// Title describes the movement in the ledger.
	Title string
	// CommandKey makes the movement idempotent. An empty key means no
	// idempotency.
	CommandKey string
	// OriginAuthKeyID and OriginSessionID carry the origin of the request, so
	// a server-generated service message is delivered like the client's own.
	OriginAuthKeyID [8]byte
	OriginSessionID int64
	// OriginUserID is the authenticated initiator when the message is
	// authored by another user, as an operator transfer is.
	OriginUserID int64
}

// WalletGramTransferResult is the result of one gram transfer.
type WalletGramTransferResult struct {
	Date      int
	Duplicate bool
	// Balance is the receiver's balance after the transfer.
	Balance int64
	// TransactionID is the sender's ledger row. The service message and the
	// history entry are the same movement, and the client correlates them by
	// this id.
	TransactionID int64
	// Send is the service message that told the chat about the transfer.
	Send SendPrivateTextResult
}