diff --git a/pkg/api/event_converters.go b/pkg/api/event_converters.go index e265a5c8..112407b7 100644 --- a/pkg/api/event_converters.go +++ b/pkg/api/event_converters.go @@ -769,7 +769,7 @@ func (h *Handler) toEvent(ctx context.Context, trace *core.Trace, result *bath.A } event.Actions[i] = convertedAction } - event.IsScam = h.spamFilter.CheckActions(event.Actions, nil, trace.Account) + event.IsScam = h.spamFilter.CheckActions(event.Actions, nil, &trace.Account) previews := make(map[tongo.AccountID]oas.JettonPreview) for _, flow := range result.ValueFlow.Accounts { for jettonMaster := range flow.Jettons { @@ -849,7 +849,7 @@ func (h *Handler) toAccountEvent(ctx context.Context, account tongo.AccountID, t e.Actions = append(e.Actions, convertedAction) } if h.spamFilter != nil { - e.IsScam = h.spamFilter.CheckActions(e.Actions, &account, trace.Account) + e.IsScam = h.spamFilter.CheckActions(e.Actions, &account, &trace.Account) } if len(e.Actions) == 0 { e.Actions = []oas.Action{ diff --git a/pkg/api/interfaces.go b/pkg/api/interfaces.go index e045da95..28be8463 100644 --- a/pkg/api/interfaces.go +++ b/pkg/api/interfaces.go @@ -75,8 +75,8 @@ type storage interface { GetJettonHolders(ctx context.Context, jettonMaster tongo.AccountID, limit, offset int) ([]core.JettonHolder, error) GetJettonMasterMetadata(ctx context.Context, master tongo.AccountID) (tongo.JettonMetadata, error) GetJettonMasterData(ctx context.Context, master tongo.AccountID) (core.JettonMaster, error) - GetAccountJettonsHistory(ctx context.Context, address tongo.AccountID, limit int, beforeLT, startTime, endTime *int64) ([]tongo.Bits256, error) - GetAccountJettonHistoryByID(ctx context.Context, address, jettonMaster tongo.AccountID, limit int, beforeLT, startTime, endTime *int64) ([]tongo.Bits256, error) + GetAccountJettonsHistory(ctx context.Context, address tongo.AccountID, limit int, beforeLT, startTime, endTime *int64) (map[core.TraceID][]core.JettonOperation, error) + GetAccountJettonHistoryByID(ctx context.Context, address, jettonMaster tongo.AccountID, limit int, beforeLT, startTime, endTime *int64) (map[core.TraceID][]core.JettonOperation, error) GetJettonTransferPayload(ctx context.Context, accountID, jettonMaster ton.AccountID) (*core.JettonTransferPayload, error) GetAllAuctions(ctx context.Context) ([]core.Auction, error) @@ -179,7 +179,7 @@ type ratesSource interface { } type SpamFilter interface { - CheckActions(actions []oas.Action, viewer *ton.AccountID, initiator ton.AccountID) bool + CheckActions(actions []oas.Action, viewer *ton.AccountID, initiator *ton.AccountID) bool JettonTrust(address tongo.AccountID, symbol, name, image string) core.TrustType NftTrust(address tongo.AccountID, collection *ton.AccountID, description, image string) core.TrustType } diff --git a/pkg/api/jetton_converters.go b/pkg/api/jetton_converters.go index 61ba34d6..916cff62 100644 --- a/pkg/api/jetton_converters.go +++ b/pkg/api/jetton_converters.go @@ -2,9 +2,13 @@ package api import ( "context" + "encoding/json" "errors" "fmt" + "github.com/tonkeeper/tongo/abi" + "github.com/tonkeeper/tongo/tlb" "net/http" + "sort" "strings" "github.com/tonkeeper/opentonapi/pkg/bath" @@ -51,43 +55,60 @@ func jettonMetadata(account ton.AccountID, meta NormalizedMetadata) oas.JettonMe return metadata } -func (h *Handler) convertJettonHistory(ctx context.Context, account ton.AccountID, master *ton.AccountID, traceIDs []ton.Bits256, acceptLanguage oas.OptString) ([]oas.AccountEvent, int64, error) { +func (h *Handler) convertJettonHistory(ctx context.Context, account ton.AccountID, master *ton.AccountID, history map[core.TraceID][]core.JettonOperation, acceptLanguage oas.OptString) ([]oas.AccountEvent, int64, error) { var lastLT uint64 - events := make([]oas.AccountEvent, 0, len(traceIDs)) - for _, traceID := range traceIDs { - trace, err := h.storage.GetTrace(ctx, traceID) - if err != nil { - if errors.Is(err, core.ErrTraceIsTooLong) { - // we ignore this for now, because we believe that this case is extremely rare. - continue - } - return nil, 0, err - } - result, err := bath.FindActions(ctx, trace, - bath.WithStraws(bath.JettonTransfersBurnsMints), - bath.WithInformationSource(h.storage)) - if err != nil { - return nil, 0, err - } + var events []oas.AccountEvent + + for id, ops := range history { event := oas.AccountEvent{ - EventID: trace.Hash.Hex(), - Account: convertAccountAddress(account, h.addressBook), - Timestamp: trace.Utime, - IsScam: false, - Lt: int64(trace.Lt), - InProgress: trace.InProgress(), - Extra: result.Extra(account), + EventID: id.Hash.Hex(), + Account: convertAccountAddress(account, h.addressBook), + Timestamp: id.UTime, // TODO: or first/last op Utime + IsScam: false, + Lt: int64(id.Lt), // TODO: or first/last op Lt + Extra: 0, } - for _, action := range result.Actions { - if action.Type != bath.JettonTransfer && action.Type != bath.JettonBurn && action.Type != bath.JettonMint { - continue - } - if master != nil && ((action.JettonTransfer != nil && action.JettonTransfer.Jetton != *master) || - (action.JettonMint != nil && action.JettonMint.Jetton != *master) || - (action.JettonBurn != nil && action.JettonBurn.Jetton != *master)) { - continue - } - if !action.IsSubject(account) { + for _, op := range ops { + var action bath.Action + switch op.Operation { + case core.TransferJettonOperation: + transferAction := bath.JettonTransferAction{ + Jetton: op.JettonMaster, + Recipient: op.Destination, + Sender: op.Source, + Amount: tlb.VarUInteger16(*op.Amount.BigInt()), + } + action.Type = "JettonTransfer" + action.JettonTransfer = &transferAction + var payload abi.JettonPayload + err := json.Unmarshal([]byte(op.ForwardPayload), &payload) + if err != nil { + break + } + switch p := payload.Value.(type) { + case abi.TextCommentJettonPayload: + comment := string(p.Text) + action.JettonTransfer.Comment = &comment + case abi.EncryptedTextCommentJettonPayload: + action.JettonTransfer.EncryptedComment = &bath.EncryptedComment{EncryptionType: "simple", CipherText: p.CipherText} + } + case core.MintJettonOperation: + mintAction := bath.JettonMintAction{ + Jetton: op.JettonMaster, + Recipient: *op.Destination, + Amount: tlb.VarUInteger16(*op.Amount.BigInt()), + } + action.Type = "JettonMint" + action.JettonMint = &mintAction + case core.BurnJettonOperation: + burnAction := bath.JettonBurnAction{ + Jetton: op.JettonMaster, + Sender: *op.Source, + Amount: tlb.VarUInteger16(*op.Amount.BigInt()), + } + action.Type = "JettonTransfer" + action.JettonBurn = &burnAction + default: continue } convertedAction, err := h.convertAction(ctx, &account, action, acceptLanguage) @@ -95,15 +116,22 @@ func (h *Handler) convertJettonHistory(ctx context.Context, account ton.AccountI return nil, 0, err } event.Actions = append(event.Actions, convertedAction) + if lastLT == 0 { + lastLT = op.Lt + } + if op.Lt < lastLT { + lastLT = op.Lt + } } - event.IsScam = h.spamFilter.CheckActions(event.Actions, &account, trace.Account) if len(event.Actions) == 0 { continue } + event.IsScam = h.spamFilter.CheckActions(event.Actions, &account, nil) events = append(events, event) - lastLT = trace.Lt } - + sort.Slice(events, func(i, j int) bool { + return events[i].Lt > events[j].Lt + }) return events, int64(lastLT), nil } diff --git a/pkg/api/jetton_handlers.go b/pkg/api/jetton_handlers.go index 20756222..5c217434 100644 --- a/pkg/api/jetton_handlers.go +++ b/pkg/api/jetton_handlers.go @@ -100,11 +100,11 @@ func (h *Handler) GetAccountJettonsHistory(ctx context.Context, params oas.GetAc if err != nil { return nil, toError(http.StatusBadRequest, err) } - traceIDs, err := h.storage.GetAccountJettonsHistory(ctx, account.ID, params.Limit, optIntToPointer(params.BeforeLt), optIntToPointer(params.StartDate), optIntToPointer(params.EndDate)) + history, err := h.storage.GetAccountJettonsHistory(ctx, account.ID, params.Limit, optIntToPointer(params.BeforeLt), optIntToPointer(params.StartDate), optIntToPointer(params.EndDate)) if err != nil { return nil, toError(http.StatusInternalServerError, err) } - events, lastLT, err := h.convertJettonHistory(ctx, account.ID, nil, traceIDs, params.AcceptLanguage) + events, lastLT, err := h.convertJettonHistory(ctx, account.ID, nil, history, params.AcceptLanguage) if err != nil { return nil, toError(http.StatusInternalServerError, err) } @@ -120,14 +120,14 @@ func (h *Handler) GetAccountJettonHistoryByID(ctx context.Context, params oas.Ge if err != nil { return nil, toError(http.StatusBadRequest, err) } - traceIDs, err := h.storage.GetAccountJettonHistoryByID(ctx, account.ID, jettonMasterAccount.ID, params.Limit, optIntToPointer(params.BeforeLt), optIntToPointer(params.StartDate), optIntToPointer(params.EndDate)) + history, err := h.storage.GetAccountJettonHistoryByID(ctx, account.ID, jettonMasterAccount.ID, params.Limit, optIntToPointer(params.BeforeLt), optIntToPointer(params.StartDate), optIntToPointer(params.EndDate)) if errors.Is(err, core.ErrEntityNotFound) { return &oas.AccountEvents{}, nil } if err != nil { return nil, toError(http.StatusInternalServerError, err) } - events, lastLT, err := h.convertJettonHistory(ctx, account.ID, &jettonMasterAccount.ID, traceIDs, params.AcceptLanguage) + events, lastLT, err := h.convertJettonHistory(ctx, account.ID, &jettonMasterAccount.ID, history, params.AcceptLanguage) if err != nil { return nil, toError(http.StatusInternalServerError, err) } diff --git a/pkg/api/nft_converters.go b/pkg/api/nft_converters.go index 7861fd81..ffa410db 100644 --- a/pkg/api/nft_converters.go +++ b/pkg/api/nft_converters.go @@ -163,7 +163,7 @@ func (h *Handler) convertNftHistory(ctx context.Context, account tongo.AccountID } event.Actions = append(event.Actions, convertedAction) } - event.IsScam = h.spamFilter.CheckActions(event.Actions, &account, trace.Account) + event.IsScam = h.spamFilter.CheckActions(event.Actions, &account, &trace.Account) if len(event.Actions) > 0 { events = append(events, event) lastLT = trace.Lt diff --git a/pkg/core/jetton.go b/pkg/core/jetton.go index 56c370ef..d3d243fd 100644 --- a/pkg/core/jetton.go +++ b/pkg/core/jetton.go @@ -37,3 +37,26 @@ type JettonWalletLockData struct { FullBalance decimal.Decimal UnlockTime int64 } + +type JettonOperationType = string + +const ( + TransferJettonOperation JettonOperationType = "transfer" + MintJettonOperation JettonOperationType = "mint" + BurnJettonOperation JettonOperationType = "burn" + UnknownJettonOperation JettonOperationType = "unknown" +) + +type JettonOperation struct { + Operation JettonOperationType + Source *tongo.AccountID + Destination *tongo.AccountID + JettonMaster tongo.AccountID + TraceID TraceID + DestEndBalance decimal.Decimal + Amount decimal.Decimal + QueryID uint64 + ForwardPayload string + Lt uint64 + Utime int64 +} diff --git a/pkg/litestorage/jetton.go b/pkg/litestorage/jetton.go index edcb950d..91f7537a 100644 --- a/pkg/litestorage/jetton.go +++ b/pkg/litestorage/jetton.go @@ -117,11 +117,11 @@ func (s *LiteStorage) GetJettonMasterData(ctx context.Context, master tongo.Acco return jettonMaster, nil } -func (s *LiteStorage) GetAccountJettonsHistory(ctx context.Context, address tongo.AccountID, limit int, beforeLT, startTime, endTime *int64) ([]tongo.Bits256, error) { +func (s *LiteStorage) GetAccountJettonsHistory(ctx context.Context, address tongo.AccountID, limit int, beforeLT, startTime, endTime *int64) (map[core.TraceID][]core.JettonOperation, error) { return nil, nil } -func (s *LiteStorage) GetAccountJettonHistoryByID(ctx context.Context, address, jettonMaster tongo.AccountID, limit int, beforeLT, startTime, endTime *int64) ([]tongo.Bits256, error) { +func (s *LiteStorage) GetAccountJettonHistoryByID(ctx context.Context, address, jettonMaster tongo.AccountID, limit int, beforeLT, startTime, endTime *int64) (map[core.TraceID][]core.JettonOperation, error) { return nil, nil } diff --git a/pkg/spam/spam.go b/pkg/spam/spam.go index 188cc833..945f5cc9 100644 --- a/pkg/spam/spam.go +++ b/pkg/spam/spam.go @@ -18,7 +18,7 @@ func NewSpamFilter() *Filter { } } -func (f Filter) CheckActions(actions []oas.Action, viewer *ton.AccountID, initiator ton.AccountID) bool { +func (f Filter) CheckActions(actions []oas.Action, viewer *ton.AccountID, initiator *ton.AccountID) bool { var comment string for _, action := range actions { switch {