Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions internal/api/user_transactions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package api

import (
"encoding/json"
"net/http"
"time"

"github.com/LightningTipBot/LightningTipBot/internal/telegram"
log "github.com/sirupsen/logrus"
)

// UserTransactionsRequest represents the JSON request for the user transactions API
type UserTransactionsRequest struct {
TelegramID int64 `json:"telegram_id"` // Telegram user ID
Limit int `json:"limit"` // Optional: max transactions to return (default 100, max 250)
Offset int `json:"offset"` // Optional: number of transactions to skip for pagination
}

// UserTransactionData represents a single internal transaction in the response
type UserTransactionData struct {
ID uint `json:"id"`
Time string `json:"time"`
Direction string `json:"direction"` // "incoming" or "outgoing" relative to the queried user
FromID int64 `json:"from_id"`
ToID int64 `json:"to_id"`
FromUser string `json:"from_user"`
ToUser string `json:"to_user"`
Type string `json:"type"`
Amount int64 `json:"amount"` // Amount in satoshis
AmountLKR string `json:"amount_lkr,omitempty"`
Memo string `json:"memo,omitempty"`
Success bool `json:"success"`
}

// UserTransactionsResponse represents the JSON response for the user transactions API
type UserTransactionsResponse struct {
Success bool `json:"success"`
TelegramID int64 `json:"telegram_id"`
Count int `json:"count"`
Limit int `json:"limit"`
Offset int `json:"offset"`
Transactions []UserTransactionData `json:"transactions"`
Message string `json:"message,omitempty"`
}

// UserTransactions handles the /api/v1/usertransactions endpoint for fetching a user's
// internal transaction history by Telegram ID. Authenticated with the same wallet HMAC
// used by the send API.
func (s Service) UserTransactions(w http.ResponseWriter, r *http.Request) {
var req UserTransactionsRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
log.Errorf("[api/usertransactions] Invalid JSON request: %v", err)
RespondError(w, "Invalid JSON request")
return
}

// Validate request
if req.TelegramID <= 0 {
RespondError(w, "Invalid Telegram ID")
return
}

// Apply pagination defaults and caps (shared with the analytics API)
limit := req.Limit
if limit <= 0 {
limit = 100
}
if limit > maxAnalyticsLimit {
limit = maxAnalyticsLimit
}
offset := req.Offset
if offset < 0 {
offset = 0
}
if offset > maxAnalyticsOffset {
offset = maxAnalyticsOffset
}

// Confirm the user exists so we can distinguish "unknown user" from "no transactions"
if _, err := telegram.GetUserByTelegramID(req.TelegramID, *s.Bot); err != nil {
log.Warnf("[api/usertransactions] User not found for Telegram ID %d: %v", req.TelegramID, err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(UserTransactionsResponse{
Success: false,
TelegramID: req.TelegramID,
Count: 0,
Limit: limit,
Offset: offset,
Transactions: []UserTransactionData{},
Message: "User not found or has no wallet",
})
return
}

// Fetch internal transactions where the user is either sender or recipient
var internalTxs []telegram.Transaction
dbTx := s.Bot.DB.Transactions.
Where("from_id = ? OR to_id = ?", req.TelegramID, req.TelegramID).
Order("time desc").
Limit(limit).Offset(offset).
Find(&internalTxs)
if dbTx.Error != nil {
log.Errorf("[api/usertransactions] Database error fetching transactions for user %d: %v", req.TelegramID, dbTx.Error)
RespondError(w, "Failed to retrieve transactions")
return
}

transactions := make([]UserTransactionData, 0, len(internalTxs))
for _, tx := range internalTxs {
direction := "incoming"
if tx.FromId == req.TelegramID {
direction = "outgoing"
}
transactions = append(transactions, UserTransactionData{
ID: tx.ID,
Time: tx.Time.Format(time.RFC3339),
Direction: direction,
FromID: tx.FromId,
ToID: tx.ToId,
FromUser: tx.FromUser,
ToUser: tx.ToUser,
Type: tx.Type,
Amount: tx.Amount,
AmountLKR: getLKRValue(tx.Amount),
Memo: tx.Memo,
Success: tx.Success,
})
}

log.Infof("[api/usertransactions] Retrieved %d transaction(s) for user %d", len(transactions), req.TelegramID)

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(UserTransactionsResponse{
Success: true,
TelegramID: req.TelegramID,
Count: len(transactions),
Limit: limit,
Offset: offset,
Transactions: transactions,
Message: "Transactions retrieved successfully",
})
}
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ func startApiServer(bot *telegram.TipBot) {
s.AppendRoute(`/api/v1/userbalance`, api.WalletHMACMiddleware(apiService.UserBalance), http.MethodPost)
log.Infof("API UserBalance endpoint registered at /api/v1/userbalance with wallet-based HMAC security")

// User transactions endpoint with wallet-based HMAC security
s.AppendRoute(`/api/v1/usertransactions`, api.WalletHMACMiddleware(apiService.UserTransactions), http.MethodPost)
log.Infof("API UserTransactions endpoint registered at /api/v1/usertransactions with wallet-based HMAC security")

// Referral lookup endpoint with wallet-based HMAC security
s.AppendRoute(`/api/v1/referral/lookup`, api.WalletHMACMiddleware(apiService.ReferralLookup), http.MethodGet)
log.Infof("API ReferralLookup endpoint registered at /api/v1/referral/lookup with wallet-based HMAC security")
Expand Down
Loading