Ctrl K
ComunidadGuías…

internal/app/wallet/transfer.go

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

Archivo original ↗Preguntar a KiBot
package wallet

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"io"
	"strings"

	"telesrv/internal/domain"
)

var ErrInvalidTransferBody = errors.New("invalid internal transfer envelope")
var ErrTransferDisabled = errors.New("internal wallet transfers are disabled")

// TransferIntent is an internal-ledger request, never a TON transaction.
// The one-use account.getTmpPassword proof is consumed by the ledger transaction.
type TransferIntent struct {
	RecipientUserID int64
	RandomID        int64
	AmountNanoton   int64
	Comment         string
	TmpPassword     []byte
}

// ParseTransferBody accepts only the versioned, password-authorized extension.
// Legacy JSON and both signed and unsigned TON BOCs are deliberately unsupported:
// wallet keys/seqnos are not authority to spend this deployment's internal ledger.
func ParseTransferBody(body []byte) (TransferIntent, error) {
	if len(body) == 0 || len(body) > 4096 {
		return TransferIntent{}, ErrInvalidTransferBody
	}
	var wire struct {
		Version         int    `json:"version"`
		RecipientUserID int64  `json:"recipient_user_id"`
		RandomID        int64  `json:"random_id"`
		AmountNanoton   int64  `json:"amount_nanoton"`
		Comment         string `json:"comment,omitempty"`
		TmpPassword     string `json:"tmp_password"`
	}
	d := json.NewDecoder(bytes.NewReader(body))
	d.DisallowUnknownFields()
	if d.Decode(&wire) != nil || d.Decode(new(any)) != io.EOF || wire.Version != 1 || wire.RecipientUserID <= 0 || wire.RandomID == 0 || wire.AmountNanoton <= 0 {
		return TransferIntent{}, ErrInvalidTransferBody
	}
	secret, err := base64.StdEncoding.Strict().DecodeString(wire.TmpPassword)
	if err != nil || len(secret) != 32 {
		return TransferIntent{}, domain.ErrTmpPasswordInvalid
	}
	return TransferIntent{RecipientUserID: wire.RecipientUserID, RandomID: wire.RandomID, AmountNanoton: wire.AmountNanoton, Comment: sanitizeTransferComment(wire.Comment), TmpPassword: secret}, nil
}

const maxTransferCommentRunes = 140

func sanitizeTransferComment(raw string) string {
	cleaned := strings.Map(func(r rune) rune {
		if r < 0x20 || r == 0x7f {
			return -1
		}
		return r
	}, strings.TrimSpace(raw))
	runes := []rune(cleaned)
	if len(runes) > maxTransferCommentRunes {
		runes = runes[:maxTransferCommentRunes]
	}
	return strings.TrimSpace(string(runes))
}

type TransferStore interface {
	TransferWalletGram(context.Context, domain.WalletGramTransferRequest) (domain.WalletGramTransferResult, error)
}

// WithInternalTransfers is an explicit opt-in. Identity/read capabilities alone
// never enable debits, and the service fails closed without the atomic store.
func WithInternalTransfers(enabled bool, store TransferStore) ServiceOption {
	return func(s *Service) {
		if enabled {
			s.transfers = store
		}
	}
}

func (s *Service) TransferWalletGram(ctx context.Context, req domain.WalletGramTransferRequest) (domain.WalletGramTransferResult, error) {
	if s == nil || s.transfers == nil {
		return domain.WalletGramTransferResult{}, ErrTransferDisabled
	}
	return s.transfers.TransferWalletGram(ctx, req)
}