diff --git a/pkg/api/event_handlers.go b/pkg/api/event_handlers.go index b9753554..43cacfe2 100644 --- a/pkg/api/event_handlers.go +++ b/pkg/api/event_handlers.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "errors" "fmt" + "github.com/tonkeeper/tongo/abi" "go.uber.org/zap" "golang.org/x/exp/slices" "net/http" @@ -390,29 +391,43 @@ func (h *Handler) EmulateMessageToAccountEvent(ctx context.Context, request *oas if err != nil { return nil, toError(http.StatusBadRequest, err) } - configBase64, err := h.storage.TrimmedConfigBase64() + hash, err := c.HashString() if err != nil { - return nil, toError(http.StatusInternalServerError, err) - } - options := []txemulator.TraceOption{ - txemulator.WithAccountsSource(h.storage), - txemulator.WithConfigBase64(configBase64), - txemulator.WithLimit(1100), - } - if !params.IgnoreSignatureCheck.Value { - options = append(options, txemulator.WithSignatureCheck()) - } - emulator, err := txemulator.NewTraceBuilder(options...) - if err != nil { - return nil, toError(http.StatusInternalServerError, err) + return nil, toError(http.StatusBadRequest, err) } - tree, err := emulator.Run(ctx, m) + trace, version, _, err := h.storage.GetTraceWithState(ctx, hash) if err != nil { - return nil, toProperEmulationError(err) + h.logger.Warn("get trace from storage: ", zap.Error(err)) } - trace, err := emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool) - if err != nil { - return nil, toError(http.StatusInternalServerError, err) + if trace == nil || h.tongoVersion == 0 || version > h.tongoVersion { + configBase64, err := h.storage.TrimmedConfigBase64() + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + options := []txemulator.TraceOption{ + txemulator.WithAccountsSource(h.storage), + txemulator.WithConfigBase64(configBase64), + txemulator.WithLimit(1100), + } + if !params.IgnoreSignatureCheck.Value { + options = append(options, txemulator.WithSignatureCheck()) + } + emulator, err := txemulator.NewTraceBuilder(options...) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + tree, err := emulator.Run(ctx, m) + if err != nil { + return nil, toProperEmulationError(err) + } + trace, err = emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + err = h.storage.SaveTraceWithState(ctx, hash, trace, h.tongoVersion, []abi.MethodInvocation{}, 24*time.Hour) + if err != nil { + fmt.Println("trace not saved: ", err) + } } actions, err := bath.FindActions(ctx, trace, bath.WithInformationSource(h.storage)) if err != nil { @@ -437,33 +452,47 @@ func (h *Handler) EmulateMessageToEvent(ctx context.Context, request *oas.Emulat } trace, prs := h.mempoolEmulate.traces.Get(hash) if !prs { - var m tlb.Message - if err := tlb.Unmarshal(c, &m); err != nil { + hs, err := c.HashString() + if err != nil { return nil, toError(http.StatusBadRequest, err) } - configBase64, err := h.storage.TrimmedConfigBase64() + trace, version, _, err := h.storage.GetTraceWithState(ctx, hs) if err != nil { - return nil, toError(http.StatusInternalServerError, err) - } - options := []txemulator.TraceOption{ - txemulator.WithAccountsSource(h.storage), - txemulator.WithConfigBase64(configBase64), - } - if !params.IgnoreSignatureCheck.Value { - options = append(options, txemulator.WithSignatureCheck()) + h.logger.Warn("get trace from storage: ", zap.Error(err)) } + if trace == nil || h.tongoVersion == 0 || version > h.tongoVersion { + var m tlb.Message + if err := tlb.Unmarshal(c, &m); err != nil { + return nil, toError(http.StatusBadRequest, err) + } + configBase64, err := h.storage.TrimmedConfigBase64() + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + options := []txemulator.TraceOption{ + txemulator.WithAccountsSource(h.storage), + txemulator.WithConfigBase64(configBase64), + } + if !params.IgnoreSignatureCheck.Value { + options = append(options, txemulator.WithSignatureCheck()) + } - emulator, err := txemulator.NewTraceBuilder(options...) - if err != nil { - return nil, toError(http.StatusInternalServerError, err) - } - tree, err := emulator.Run(ctx, m) - if err != nil { - return nil, toProperEmulationError(err) - } - trace, err = emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool) - if err != nil { - return nil, toError(http.StatusInternalServerError, err) + emulator, err := txemulator.NewTraceBuilder(options...) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + tree, err := emulator.Run(ctx, m) + if err != nil { + return nil, toProperEmulationError(err) + } + trace, err = emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + err = h.storage.SaveTraceWithState(ctx, hs, trace, h.tongoVersion, []abi.MethodInvocation{}, 24*time.Hour) + if err != nil { + fmt.Println("trace not saved: ", err) + } } } actions, err := bath.FindActions(ctx, trace, bath.WithInformationSource(h.storage)) @@ -489,34 +518,48 @@ func (h *Handler) EmulateMessageToTrace(ctx context.Context, request *oas.Emulat } trace, prs := h.mempoolEmulate.traces.Get(hash) if !prs { - var m tlb.Message - err = tlb.Unmarshal(c, &m) + hs, err := c.HashString() if err != nil { return nil, toError(http.StatusBadRequest, err) } - configBase64, err := h.storage.TrimmedConfigBase64() + trace, version, _, err := h.storage.GetTraceWithState(ctx, hs) if err != nil { - return nil, toError(http.StatusInternalServerError, err) - } - options := []txemulator.TraceOption{ - txemulator.WithAccountsSource(h.storage), - txemulator.WithConfigBase64(configBase64), - } - if !params.IgnoreSignatureCheck.Value { - options = append(options, txemulator.WithSignatureCheck()) + h.logger.Warn("get trace from storage: ", zap.Error(err)) } + if trace == nil || h.tongoVersion == 0 || version > h.tongoVersion { + var m tlb.Message + err = tlb.Unmarshal(c, &m) + if err != nil { + return nil, toError(http.StatusBadRequest, err) + } + configBase64, err := h.storage.TrimmedConfigBase64() + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + options := []txemulator.TraceOption{ + txemulator.WithAccountsSource(h.storage), + txemulator.WithConfigBase64(configBase64), + } + if !params.IgnoreSignatureCheck.Value { + options = append(options, txemulator.WithSignatureCheck()) + } - emulator, err := txemulator.NewTraceBuilder(options...) - if err != nil { - return nil, toError(http.StatusInternalServerError, err) - } - tree, err := emulator.Run(ctx, m) - if err != nil { - return nil, toProperEmulationError(err) - } - trace, err = emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool) - if err != nil { - return nil, toError(http.StatusInternalServerError, err) + emulator, err := txemulator.NewTraceBuilder(options...) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + tree, err := emulator.Run(ctx, m) + if err != nil { + return nil, toProperEmulationError(err) + } + trace, err = emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + err = h.storage.SaveTraceWithState(ctx, hs, trace, h.tongoVersion, []abi.MethodInvocation{}, 24*time.Hour) + if err != nil { + fmt.Println("trace not saved: ", err) + } } } t := convertTrace(trace, h.addressBook) @@ -603,44 +646,60 @@ func (h *Handler) EmulateMessageToWallet(ctx context.Context, request *oas.Emula if err != nil { return nil, toError(http.StatusInternalServerError, err) } - configBase64, err := h.storage.TrimmedConfigBase64() - if err != nil { - return nil, toError(http.StatusInternalServerError, err) - } - options := []txemulator.TraceOption{ - txemulator.WithConfigBase64(configBase64), - txemulator.WithAccountsSource(h.storage), - txemulator.WithLimit(1100), - } - accounts, err := convertEmulationParameters(request.Params) + hash, err := msgCell.HashString() if err != nil { return nil, toError(http.StatusBadRequest, err) } - var states []tlb.ShardAccount - for accountID, balance := range accounts { - originalState, err := h.storage.GetAccountState(ctx, accountID) + trace, version, _, err := h.storage.GetTraceWithState(ctx, hash) + if err != nil { + h.logger.Warn("get trace from storage: ", zap.Error(err)) + } + if trace == nil || h.tongoVersion == 0 || version > h.tongoVersion { + configBase64, err := h.storage.TrimmedConfigBase64() if err != nil { return nil, toError(http.StatusInternalServerError, err) } - state, err := prepareAccountState(*walletAddress, originalState, balance) + + options := []txemulator.TraceOption{ + txemulator.WithConfigBase64(configBase64), + txemulator.WithAccountsSource(h.storage), + txemulator.WithLimit(1100), + } + accounts, err := convertEmulationParameters(request.Params) + if err != nil { + return nil, toError(http.StatusBadRequest, err) + } + var states []tlb.ShardAccount + for accountID, balance := range accounts { + originalState, err := h.storage.GetAccountState(ctx, accountID) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + state, err := prepareAccountState(*walletAddress, originalState, balance) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + states = append(states, state) + } + + options = append(options, txemulator.WithAccounts(states...)) + emulator, err := txemulator.NewTraceBuilder(options...) if err != nil { return nil, toError(http.StatusInternalServerError, err) } - states = append(states, state) - } - options = append(options, txemulator.WithAccounts(states...)) - emulator, err := txemulator.NewTraceBuilder(options...) - if err != nil { - return nil, toError(http.StatusInternalServerError, err) - } - tree, err := emulator.Run(ctx, m) - if err != nil { - return nil, toProperEmulationError(err) - } - trace, err := emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool) - if err != nil { - return nil, toError(http.StatusInternalServerError, err) + tree, err := emulator.Run(ctx, m) + if err != nil { + return nil, toProperEmulationError(err) + } + trace, err = emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + err = h.storage.SaveTraceWithState(ctx, hash, trace, h.tongoVersion, []abi.MethodInvocation{}, 24*time.Hour) + if err != nil { + fmt.Println("trace not saved: ", err) + } } t := convertTrace(trace, h.addressBook) actions, err := bath.FindActions(ctx, trace, bath.ForAccount(*walletAddress), bath.WithInformationSource(h.storage)) diff --git a/pkg/api/handler.go b/pkg/api/handler.go index 9f2fc0c1..96a7988c 100644 --- a/pkg/api/handler.go +++ b/pkg/api/handler.go @@ -3,6 +3,7 @@ package api import ( "context" "fmt" + "golang.org/x/exp/slog" "sync" "github.com/go-faster/errors" @@ -51,6 +52,7 @@ type Handler struct { mempoolEmulate mempoolEmulate // ctxToDetails converts a request context to a details instance. ctxToDetails ctxToDetails + tongoVersion int // mempoolEmulateIgnoreAccounts, we don't track pending transactions for this list of accounts. mempoolEmulateIgnoreAccounts map[tongo.AccountID]struct{} @@ -218,6 +220,10 @@ func NewHandler(logger *zap.Logger, opts ...Option) (*Handler, error) { if options.score == nil { options.score = score.NewScore() } + tongoVersion, err := GetPackageVersionInt("tongo") + if err != nil { + slog.Warn("unable to detect tongo version", "err", err) + } return &Handler{ logger: logger, storage: options.storage, @@ -244,6 +250,7 @@ func NewHandler(logger *zap.Logger, opts ...Option) (*Handler, error) { mempoolEmulateIgnoreAccounts: map[tongo.AccountID]struct{}{ tongo.MustParseAddress("0:0000000000000000000000000000000000000000000000000000000000000000").ID: {}, }, + tongoVersion: tongoVersion, blacklistedBocCache: cache.NewLRUCache[[32]byte, struct{}](100000, "blacklisted_boc_cache"), getMethodsCache: cache.NewLRUCache[string, *oas.MethodExecutionResult](100000, "get_methods_cache"), tonConnect: tonConnect, diff --git a/pkg/api/interfaces.go b/pkg/api/interfaces.go index 85e6d8ef..f702f64a 100644 --- a/pkg/api/interfaces.go +++ b/pkg/api/interfaces.go @@ -3,6 +3,7 @@ package api import ( "context" "crypto/ed25519" + "time" "github.com/tonkeeper/opentonapi/pkg/gasless" "github.com/tonkeeper/opentonapi/pkg/oas" @@ -111,6 +112,9 @@ type storage interface { GetAccountMultisigs(ctx context.Context, accountID ton.AccountID) ([]core.Multisig, error) GetMultisigByID(ctx context.Context, accountID ton.AccountID) (*core.Multisig, error) + SaveTraceWithState(ctx context.Context, msgHash string, trace *core.Trace, version int, getMethods []abi.MethodInvocation, ttl time.Duration) error + GetTraceWithState(ctx context.Context, msgHash string) (*core.Trace, int, []abi.MethodInvocation, error) + liteStorageRaw } diff --git a/pkg/api/utils.go b/pkg/api/utils.go index 3249b98b..bc15233b 100644 --- a/pkg/api/utils.go +++ b/pkg/api/utils.go @@ -2,6 +2,9 @@ package api import ( "fmt" + "runtime/debug" + "strconv" + "strings" "github.com/tonkeeper/tongo/boc" ) @@ -25,3 +28,31 @@ func deserializeSingleBoc(bocStr string) (*boc.Cell, error) { } return cells[0], nil } + +func GetPackageVersionInt(packagePath string) (int, error) { + info, ok := debug.ReadBuildInfo() + if !ok { + return 0, fmt.Errorf("error getting build info") + } + + for _, dep := range info.Deps { + if strings.Contains(dep.Path, packagePath) { + version := strings.TrimPrefix(dep.Version, "v") + + parts := strings.Split(version, ".") + + result := 0 + for _, part := range parts { + num, err := strconv.Atoi(part) + if err != nil { + return 0, fmt.Errorf("error parsing version number: %v", err) + } + result = result*100 + num + } + + return result, nil + } + } + + return 0, fmt.Errorf("package %s not found", packagePath) +} diff --git a/pkg/core/trace.go b/pkg/core/trace.go index 1f26effa..f96713e7 100644 --- a/pkg/core/trace.go +++ b/pkg/core/trace.go @@ -2,7 +2,9 @@ package core import ( "context" + "encoding/json" "errors" + "github.com/tonkeeper/tongo/ton" "sync" "github.com/shopspring/decimal" @@ -65,6 +67,58 @@ func (t *Trace) SetAdditionalInfo(info *TraceAdditionalInfo) { t.additionalInfo = info } +func (t *TraceAdditionalInfo) MarshalJSON() ([]byte, error) { + type Alias struct { + JettonMasters map[string]string `json:",omitempty"` + NftSaleContract *NftSaleContract `json:",omitempty"` + STONfiPool *STONfiPool `json:",omitempty"` + EmulatedTeleitemNFT *EmulatedTeleitemNFT `json:",omitempty"` + } + + masters := make(map[string]string) + if t.JettonMasters != nil { + for k, v := range t.JettonMasters { + masters[k.String()] = v.String() + } + } + + return json.Marshal(&Alias{ + JettonMasters: masters, + NftSaleContract: t.NftSaleContract, + STONfiPool: t.STONfiPool, + EmulatedTeleitemNFT: t.EmulatedTeleitemNFT, + }) +} + +func (t *TraceAdditionalInfo) UnmarshalJSON(data []byte) error { + type Alias struct { + JettonMasters map[string]string `json:",omitempty"` + NftSaleContract *NftSaleContract `json:",omitempty"` + STONfiPool *STONfiPool `json:",omitempty"` + EmulatedTeleitemNFT *EmulatedTeleitemNFT `json:",omitempty"` + } + + aux := &Alias{} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if aux.JettonMasters != nil { + t.JettonMasters = make(map[tongo.AccountID]tongo.AccountID) + for kStr, vStr := range aux.JettonMasters { + key := ton.MustParseAccountID(kStr) + val := ton.MustParseAccountID(vStr) + t.JettonMasters[key] = val + } + } + + t.NftSaleContract = aux.NftSaleContract + t.STONfiPool = aux.STONfiPool + t.EmulatedTeleitemNFT = aux.EmulatedTeleitemNFT + + return nil +} + func (t *Trace) InProgress() bool { return t.countUncompleted() != 0 } diff --git a/pkg/core/transactions.go b/pkg/core/transactions.go index b6b6db24..22c8e88f 100644 --- a/pkg/core/transactions.go +++ b/pkg/core/transactions.go @@ -1,9 +1,11 @@ package core import ( + "encoding/json" "fmt" "github.com/tonkeeper/tongo/ton" "math/big" + "reflect" "strings" "github.com/tonkeeper/tongo/abi" @@ -176,6 +178,43 @@ type Message struct { DecodedBody *DecodedMessageBody } +func (m *Message) UnmarshalJSON(data []byte) error { + type Alias Message + r := struct { + *Alias + DecodedBody struct { + Operation string + Value json.RawMessage + } + }{ + Alias: (*Alias)(m), + } + if err := json.Unmarshal(data, &r); err != nil { + return err + } + var msgTypes map[string]any + switch r.MsgType { + case IntMsg: + msgTypes = abi.KnownMsgInTypes + case ExtOutMsg: + msgTypes = abi.KnownMsgExtOutTypes + case ExtInMsg: + msgTypes = abi.KnownMsgExtInTypes + } + if v, present := msgTypes[r.DecodedBody.Operation]; present { + p := reflect.New(reflect.TypeOf(v)) + if err := json.Unmarshal(r.DecodedBody.Value, p.Interface()); err != nil { + return err + } + m.DecodedBody = &DecodedMessageBody{ + Operation: r.DecodedBody.Operation, + Value: p.Elem().Interface(), + } + return nil + } + return nil +} + // DecodedMessageBody contains a message body decoded by tongo.abi package. type DecodedMessageBody struct { Operation string diff --git a/pkg/litestorage/litestorage.go b/pkg/litestorage/litestorage.go index b914d60a..3149fa0e 100644 --- a/pkg/litestorage/litestorage.go +++ b/pkg/litestorage/litestorage.go @@ -566,3 +566,11 @@ func (s *LiteStorage) GetAccountMultisigs(ctx context.Context, accountID ton.Acc func (s *LiteStorage) GetMultisigByID(ctx context.Context, accountID ton.AccountID) (*core.Multisig, error) { return nil, fmt.Errorf("not implemented") } + +func (s *LiteStorage) SaveTraceWithState(ctx context.Context, msgHash string, trace *core.Trace, version int, getMethods []abi.MethodInvocation, ttl time.Duration) error { + return fmt.Errorf("not implemented") +} + +func (s *LiteStorage) GetTraceWithState(ctx context.Context, msgHash string) (*core.Trace, int, []abi.MethodInvocation, error) { + return nil, 0, nil, fmt.Errorf("not implemented") +}