Skip to content

Commit e66a30b

Browse files
authoredMar 11, 2025··
Merge pull request #547 from tonkeeper/emulation
Save serialized traces
2 parents e1afdc0 + b498216 commit e66a30b

File tree

7 files changed

+293
-91
lines changed

7 files changed

+293
-91
lines changed
 

‎pkg/api/event_handlers.go

+150-91
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/hex"
88
"errors"
99
"fmt"
10+
"github.com/tonkeeper/tongo/abi"
1011
"go.uber.org/zap"
1112
"golang.org/x/exp/slices"
1213
"net/http"
@@ -390,29 +391,43 @@ func (h *Handler) EmulateMessageToAccountEvent(ctx context.Context, request *oas
390391
if err != nil {
391392
return nil, toError(http.StatusBadRequest, err)
392393
}
393-
configBase64, err := h.storage.TrimmedConfigBase64()
394+
hash, err := c.HashString()
394395
if err != nil {
395-
return nil, toError(http.StatusInternalServerError, err)
396-
}
397-
options := []txemulator.TraceOption{
398-
txemulator.WithAccountsSource(h.storage),
399-
txemulator.WithConfigBase64(configBase64),
400-
txemulator.WithLimit(1100),
401-
}
402-
if !params.IgnoreSignatureCheck.Value {
403-
options = append(options, txemulator.WithSignatureCheck())
404-
}
405-
emulator, err := txemulator.NewTraceBuilder(options...)
406-
if err != nil {
407-
return nil, toError(http.StatusInternalServerError, err)
396+
return nil, toError(http.StatusBadRequest, err)
408397
}
409-
tree, err := emulator.Run(ctx, m)
398+
trace, version, _, err := h.storage.GetTraceWithState(ctx, hash)
410399
if err != nil {
411-
return nil, toProperEmulationError(err)
400+
h.logger.Warn("get trace from storage: ", zap.Error(err))
412401
}
413-
trace, err := emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool)
414-
if err != nil {
415-
return nil, toError(http.StatusInternalServerError, err)
402+
if trace == nil || h.tongoVersion == 0 || version > h.tongoVersion {
403+
configBase64, err := h.storage.TrimmedConfigBase64()
404+
if err != nil {
405+
return nil, toError(http.StatusInternalServerError, err)
406+
}
407+
options := []txemulator.TraceOption{
408+
txemulator.WithAccountsSource(h.storage),
409+
txemulator.WithConfigBase64(configBase64),
410+
txemulator.WithLimit(1100),
411+
}
412+
if !params.IgnoreSignatureCheck.Value {
413+
options = append(options, txemulator.WithSignatureCheck())
414+
}
415+
emulator, err := txemulator.NewTraceBuilder(options...)
416+
if err != nil {
417+
return nil, toError(http.StatusInternalServerError, err)
418+
}
419+
tree, err := emulator.Run(ctx, m)
420+
if err != nil {
421+
return nil, toProperEmulationError(err)
422+
}
423+
trace, err = emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool)
424+
if err != nil {
425+
return nil, toError(http.StatusInternalServerError, err)
426+
}
427+
err = h.storage.SaveTraceWithState(ctx, hash, trace, h.tongoVersion, []abi.MethodInvocation{}, 24*time.Hour)
428+
if err != nil {
429+
fmt.Println("trace not saved: ", err)
430+
}
416431
}
417432
actions, err := bath.FindActions(ctx, trace, bath.WithInformationSource(h.storage))
418433
if err != nil {
@@ -437,33 +452,47 @@ func (h *Handler) EmulateMessageToEvent(ctx context.Context, request *oas.Emulat
437452
}
438453
trace, prs := h.mempoolEmulate.traces.Get(hash)
439454
if !prs {
440-
var m tlb.Message
441-
if err := tlb.Unmarshal(c, &m); err != nil {
455+
hs, err := c.HashString()
456+
if err != nil {
442457
return nil, toError(http.StatusBadRequest, err)
443458
}
444-
configBase64, err := h.storage.TrimmedConfigBase64()
459+
trace, version, _, err := h.storage.GetTraceWithState(ctx, hs)
445460
if err != nil {
446-
return nil, toError(http.StatusInternalServerError, err)
447-
}
448-
options := []txemulator.TraceOption{
449-
txemulator.WithAccountsSource(h.storage),
450-
txemulator.WithConfigBase64(configBase64),
451-
}
452-
if !params.IgnoreSignatureCheck.Value {
453-
options = append(options, txemulator.WithSignatureCheck())
461+
h.logger.Warn("get trace from storage: ", zap.Error(err))
454462
}
463+
if trace == nil || h.tongoVersion == 0 || version > h.tongoVersion {
464+
var m tlb.Message
465+
if err := tlb.Unmarshal(c, &m); err != nil {
466+
return nil, toError(http.StatusBadRequest, err)
467+
}
468+
configBase64, err := h.storage.TrimmedConfigBase64()
469+
if err != nil {
470+
return nil, toError(http.StatusInternalServerError, err)
471+
}
472+
options := []txemulator.TraceOption{
473+
txemulator.WithAccountsSource(h.storage),
474+
txemulator.WithConfigBase64(configBase64),
475+
}
476+
if !params.IgnoreSignatureCheck.Value {
477+
options = append(options, txemulator.WithSignatureCheck())
478+
}
455479

456-
emulator, err := txemulator.NewTraceBuilder(options...)
457-
if err != nil {
458-
return nil, toError(http.StatusInternalServerError, err)
459-
}
460-
tree, err := emulator.Run(ctx, m)
461-
if err != nil {
462-
return nil, toProperEmulationError(err)
463-
}
464-
trace, err = emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool)
465-
if err != nil {
466-
return nil, toError(http.StatusInternalServerError, err)
480+
emulator, err := txemulator.NewTraceBuilder(options...)
481+
if err != nil {
482+
return nil, toError(http.StatusInternalServerError, err)
483+
}
484+
tree, err := emulator.Run(ctx, m)
485+
if err != nil {
486+
return nil, toProperEmulationError(err)
487+
}
488+
trace, err = emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool)
489+
if err != nil {
490+
return nil, toError(http.StatusInternalServerError, err)
491+
}
492+
err = h.storage.SaveTraceWithState(ctx, hs, trace, h.tongoVersion, []abi.MethodInvocation{}, 24*time.Hour)
493+
if err != nil {
494+
fmt.Println("trace not saved: ", err)
495+
}
467496
}
468497
}
469498
actions, err := bath.FindActions(ctx, trace, bath.WithInformationSource(h.storage))
@@ -489,34 +518,48 @@ func (h *Handler) EmulateMessageToTrace(ctx context.Context, request *oas.Emulat
489518
}
490519
trace, prs := h.mempoolEmulate.traces.Get(hash)
491520
if !prs {
492-
var m tlb.Message
493-
err = tlb.Unmarshal(c, &m)
521+
hs, err := c.HashString()
494522
if err != nil {
495523
return nil, toError(http.StatusBadRequest, err)
496524
}
497-
configBase64, err := h.storage.TrimmedConfigBase64()
525+
trace, version, _, err := h.storage.GetTraceWithState(ctx, hs)
498526
if err != nil {
499-
return nil, toError(http.StatusInternalServerError, err)
500-
}
501-
options := []txemulator.TraceOption{
502-
txemulator.WithAccountsSource(h.storage),
503-
txemulator.WithConfigBase64(configBase64),
504-
}
505-
if !params.IgnoreSignatureCheck.Value {
506-
options = append(options, txemulator.WithSignatureCheck())
527+
h.logger.Warn("get trace from storage: ", zap.Error(err))
507528
}
529+
if trace == nil || h.tongoVersion == 0 || version > h.tongoVersion {
530+
var m tlb.Message
531+
err = tlb.Unmarshal(c, &m)
532+
if err != nil {
533+
return nil, toError(http.StatusBadRequest, err)
534+
}
535+
configBase64, err := h.storage.TrimmedConfigBase64()
536+
if err != nil {
537+
return nil, toError(http.StatusInternalServerError, err)
538+
}
539+
options := []txemulator.TraceOption{
540+
txemulator.WithAccountsSource(h.storage),
541+
txemulator.WithConfigBase64(configBase64),
542+
}
543+
if !params.IgnoreSignatureCheck.Value {
544+
options = append(options, txemulator.WithSignatureCheck())
545+
}
508546

509-
emulator, err := txemulator.NewTraceBuilder(options...)
510-
if err != nil {
511-
return nil, toError(http.StatusInternalServerError, err)
512-
}
513-
tree, err := emulator.Run(ctx, m)
514-
if err != nil {
515-
return nil, toProperEmulationError(err)
516-
}
517-
trace, err = emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool)
518-
if err != nil {
519-
return nil, toError(http.StatusInternalServerError, err)
547+
emulator, err := txemulator.NewTraceBuilder(options...)
548+
if err != nil {
549+
return nil, toError(http.StatusInternalServerError, err)
550+
}
551+
tree, err := emulator.Run(ctx, m)
552+
if err != nil {
553+
return nil, toProperEmulationError(err)
554+
}
555+
trace, err = emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool)
556+
if err != nil {
557+
return nil, toError(http.StatusInternalServerError, err)
558+
}
559+
err = h.storage.SaveTraceWithState(ctx, hs, trace, h.tongoVersion, []abi.MethodInvocation{}, 24*time.Hour)
560+
if err != nil {
561+
fmt.Println("trace not saved: ", err)
562+
}
520563
}
521564
}
522565
t := convertTrace(trace, h.addressBook)
@@ -603,44 +646,60 @@ func (h *Handler) EmulateMessageToWallet(ctx context.Context, request *oas.Emula
603646
if err != nil {
604647
return nil, toError(http.StatusInternalServerError, err)
605648
}
606-
configBase64, err := h.storage.TrimmedConfigBase64()
607-
if err != nil {
608-
return nil, toError(http.StatusInternalServerError, err)
609-
}
610649

611-
options := []txemulator.TraceOption{
612-
txemulator.WithConfigBase64(configBase64),
613-
txemulator.WithAccountsSource(h.storage),
614-
txemulator.WithLimit(1100),
615-
}
616-
accounts, err := convertEmulationParameters(request.Params)
650+
hash, err := msgCell.HashString()
617651
if err != nil {
618652
return nil, toError(http.StatusBadRequest, err)
619653
}
620-
var states []tlb.ShardAccount
621-
for accountID, balance := range accounts {
622-
originalState, err := h.storage.GetAccountState(ctx, accountID)
654+
trace, version, _, err := h.storage.GetTraceWithState(ctx, hash)
655+
if err != nil {
656+
h.logger.Warn("get trace from storage: ", zap.Error(err))
657+
}
658+
if trace == nil || h.tongoVersion == 0 || version > h.tongoVersion {
659+
configBase64, err := h.storage.TrimmedConfigBase64()
623660
if err != nil {
624661
return nil, toError(http.StatusInternalServerError, err)
625662
}
626-
state, err := prepareAccountState(*walletAddress, originalState, balance)
663+
664+
options := []txemulator.TraceOption{
665+
txemulator.WithConfigBase64(configBase64),
666+
txemulator.WithAccountsSource(h.storage),
667+
txemulator.WithLimit(1100),
668+
}
669+
accounts, err := convertEmulationParameters(request.Params)
670+
if err != nil {
671+
return nil, toError(http.StatusBadRequest, err)
672+
}
673+
var states []tlb.ShardAccount
674+
for accountID, balance := range accounts {
675+
originalState, err := h.storage.GetAccountState(ctx, accountID)
676+
if err != nil {
677+
return nil, toError(http.StatusInternalServerError, err)
678+
}
679+
state, err := prepareAccountState(*walletAddress, originalState, balance)
680+
if err != nil {
681+
return nil, toError(http.StatusInternalServerError, err)
682+
}
683+
states = append(states, state)
684+
}
685+
686+
options = append(options, txemulator.WithAccounts(states...))
687+
emulator, err := txemulator.NewTraceBuilder(options...)
627688
if err != nil {
628689
return nil, toError(http.StatusInternalServerError, err)
629690
}
630-
states = append(states, state)
631-
}
632-
options = append(options, txemulator.WithAccounts(states...))
633-
emulator, err := txemulator.NewTraceBuilder(options...)
634-
if err != nil {
635-
return nil, toError(http.StatusInternalServerError, err)
636-
}
637-
tree, err := emulator.Run(ctx, m)
638-
if err != nil {
639-
return nil, toProperEmulationError(err)
640-
}
641-
trace, err := emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool)
642-
if err != nil {
643-
return nil, toError(http.StatusInternalServerError, err)
691+
tree, err := emulator.Run(ctx, m)
692+
if err != nil {
693+
return nil, toProperEmulationError(err)
694+
}
695+
trace, err = emulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool)
696+
if err != nil {
697+
return nil, toError(http.StatusInternalServerError, err)
698+
}
699+
err = h.storage.SaveTraceWithState(ctx, hash, trace, h.tongoVersion, []abi.MethodInvocation{}, 24*time.Hour)
700+
if err != nil {
701+
fmt.Println("trace not saved: ", err)
702+
}
644703
}
645704
t := convertTrace(trace, h.addressBook)
646705
actions, err := bath.FindActions(ctx, trace, bath.ForAccount(*walletAddress), bath.WithInformationSource(h.storage))

‎pkg/api/handler.go

+7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package api
33
import (
44
"context"
55
"fmt"
6+
"golang.org/x/exp/slog"
67
"sync"
78

89
"github.com/go-faster/errors"
@@ -51,6 +52,7 @@ type Handler struct {
5152
mempoolEmulate mempoolEmulate
5253
// ctxToDetails converts a request context to a details instance.
5354
ctxToDetails ctxToDetails
55+
tongoVersion int
5456

5557
// mempoolEmulateIgnoreAccounts, we don't track pending transactions for this list of accounts.
5658
mempoolEmulateIgnoreAccounts map[tongo.AccountID]struct{}
@@ -218,6 +220,10 @@ func NewHandler(logger *zap.Logger, opts ...Option) (*Handler, error) {
218220
if options.score == nil {
219221
options.score = score.NewScore()
220222
}
223+
tongoVersion, err := GetPackageVersionInt("tongo")
224+
if err != nil {
225+
slog.Warn("unable to detect tongo version", "err", err)
226+
}
221227
return &Handler{
222228
logger: logger,
223229
storage: options.storage,
@@ -244,6 +250,7 @@ func NewHandler(logger *zap.Logger, opts ...Option) (*Handler, error) {
244250
mempoolEmulateIgnoreAccounts: map[tongo.AccountID]struct{}{
245251
tongo.MustParseAddress("0:0000000000000000000000000000000000000000000000000000000000000000").ID: {},
246252
},
253+
tongoVersion: tongoVersion,
247254
blacklistedBocCache: cache.NewLRUCache[[32]byte, struct{}](100000, "blacklisted_boc_cache"),
248255
getMethodsCache: cache.NewLRUCache[string, *oas.MethodExecutionResult](100000, "get_methods_cache"),
249256
tonConnect: tonConnect,

‎pkg/api/interfaces.go

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package api
33
import (
44
"context"
55
"crypto/ed25519"
6+
"time"
67

78
"github.com/tonkeeper/opentonapi/pkg/gasless"
89
"github.com/tonkeeper/opentonapi/pkg/oas"
@@ -111,6 +112,9 @@ type storage interface {
111112
GetAccountMultisigs(ctx context.Context, accountID ton.AccountID) ([]core.Multisig, error)
112113
GetMultisigByID(ctx context.Context, accountID ton.AccountID) (*core.Multisig, error)
113114

115+
SaveTraceWithState(ctx context.Context, msgHash string, trace *core.Trace, version int, getMethods []abi.MethodInvocation, ttl time.Duration) error
116+
GetTraceWithState(ctx context.Context, msgHash string) (*core.Trace, int, []abi.MethodInvocation, error)
117+
114118
liteStorageRaw
115119
}
116120

‎pkg/api/utils.go

+31
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package api
22

33
import (
44
"fmt"
5+
"runtime/debug"
6+
"strconv"
7+
"strings"
58

69
"github.com/tonkeeper/tongo/boc"
710
)
@@ -25,3 +28,31 @@ func deserializeSingleBoc(bocStr string) (*boc.Cell, error) {
2528
}
2629
return cells[0], nil
2730
}
31+
32+
func GetPackageVersionInt(packagePath string) (int, error) {
33+
info, ok := debug.ReadBuildInfo()
34+
if !ok {
35+
return 0, fmt.Errorf("error getting build info")
36+
}
37+
38+
for _, dep := range info.Deps {
39+
if strings.Contains(dep.Path, packagePath) {
40+
version := strings.TrimPrefix(dep.Version, "v")
41+
42+
parts := strings.Split(version, ".")
43+
44+
result := 0
45+
for _, part := range parts {
46+
num, err := strconv.Atoi(part)
47+
if err != nil {
48+
return 0, fmt.Errorf("error parsing version number: %v", err)
49+
}
50+
result = result*100 + num
51+
}
52+
53+
return result, nil
54+
}
55+
}
56+
57+
return 0, fmt.Errorf("package %s not found", packagePath)
58+
}

‎pkg/core/trace.go

+54
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package core
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
7+
"github.com/tonkeeper/tongo/ton"
68
"sync"
79

810
"github.com/shopspring/decimal"
@@ -65,6 +67,58 @@ func (t *Trace) SetAdditionalInfo(info *TraceAdditionalInfo) {
6567
t.additionalInfo = info
6668
}
6769

70+
func (t *TraceAdditionalInfo) MarshalJSON() ([]byte, error) {
71+
type Alias struct {
72+
JettonMasters map[string]string `json:",omitempty"`
73+
NftSaleContract *NftSaleContract `json:",omitempty"`
74+
STONfiPool *STONfiPool `json:",omitempty"`
75+
EmulatedTeleitemNFT *EmulatedTeleitemNFT `json:",omitempty"`
76+
}
77+
78+
masters := make(map[string]string)
79+
if t.JettonMasters != nil {
80+
for k, v := range t.JettonMasters {
81+
masters[k.String()] = v.String()
82+
}
83+
}
84+
85+
return json.Marshal(&Alias{
86+
JettonMasters: masters,
87+
NftSaleContract: t.NftSaleContract,
88+
STONfiPool: t.STONfiPool,
89+
EmulatedTeleitemNFT: t.EmulatedTeleitemNFT,
90+
})
91+
}
92+
93+
func (t *TraceAdditionalInfo) UnmarshalJSON(data []byte) error {
94+
type Alias struct {
95+
JettonMasters map[string]string `json:",omitempty"`
96+
NftSaleContract *NftSaleContract `json:",omitempty"`
97+
STONfiPool *STONfiPool `json:",omitempty"`
98+
EmulatedTeleitemNFT *EmulatedTeleitemNFT `json:",omitempty"`
99+
}
100+
101+
aux := &Alias{}
102+
if err := json.Unmarshal(data, aux); err != nil {
103+
return err
104+
}
105+
106+
if aux.JettonMasters != nil {
107+
t.JettonMasters = make(map[tongo.AccountID]tongo.AccountID)
108+
for kStr, vStr := range aux.JettonMasters {
109+
key := ton.MustParseAccountID(kStr)
110+
val := ton.MustParseAccountID(vStr)
111+
t.JettonMasters[key] = val
112+
}
113+
}
114+
115+
t.NftSaleContract = aux.NftSaleContract
116+
t.STONfiPool = aux.STONfiPool
117+
t.EmulatedTeleitemNFT = aux.EmulatedTeleitemNFT
118+
119+
return nil
120+
}
121+
68122
func (t *Trace) InProgress() bool {
69123
return t.countUncompleted() != 0
70124
}

‎pkg/core/transactions.go

+39
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package core
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"github.com/tonkeeper/tongo/ton"
67
"math/big"
8+
"reflect"
79
"strings"
810

911
"github.com/tonkeeper/tongo/abi"
@@ -176,6 +178,43 @@ type Message struct {
176178
DecodedBody *DecodedMessageBody
177179
}
178180

181+
func (m *Message) UnmarshalJSON(data []byte) error {
182+
type Alias Message
183+
r := struct {
184+
*Alias
185+
DecodedBody struct {
186+
Operation string
187+
Value json.RawMessage
188+
}
189+
}{
190+
Alias: (*Alias)(m),
191+
}
192+
if err := json.Unmarshal(data, &r); err != nil {
193+
return err
194+
}
195+
var msgTypes map[string]any
196+
switch r.MsgType {
197+
case IntMsg:
198+
msgTypes = abi.KnownMsgInTypes
199+
case ExtOutMsg:
200+
msgTypes = abi.KnownMsgExtOutTypes
201+
case ExtInMsg:
202+
msgTypes = abi.KnownMsgExtInTypes
203+
}
204+
if v, present := msgTypes[r.DecodedBody.Operation]; present {
205+
p := reflect.New(reflect.TypeOf(v))
206+
if err := json.Unmarshal(r.DecodedBody.Value, p.Interface()); err != nil {
207+
return err
208+
}
209+
m.DecodedBody = &DecodedMessageBody{
210+
Operation: r.DecodedBody.Operation,
211+
Value: p.Elem().Interface(),
212+
}
213+
return nil
214+
}
215+
return nil
216+
}
217+
179218
// DecodedMessageBody contains a message body decoded by tongo.abi package.
180219
type DecodedMessageBody struct {
181220
Operation string

‎pkg/litestorage/litestorage.go

+8
Original file line numberDiff line numberDiff line change
@@ -566,3 +566,11 @@ func (s *LiteStorage) GetAccountMultisigs(ctx context.Context, accountID ton.Acc
566566
func (s *LiteStorage) GetMultisigByID(ctx context.Context, accountID ton.AccountID) (*core.Multisig, error) {
567567
return nil, fmt.Errorf("not implemented")
568568
}
569+
570+
func (s *LiteStorage) SaveTraceWithState(ctx context.Context, msgHash string, trace *core.Trace, version int, getMethods []abi.MethodInvocation, ttl time.Duration) error {
571+
return fmt.Errorf("not implemented")
572+
}
573+
574+
func (s *LiteStorage) GetTraceWithState(ctx context.Context, msgHash string) (*core.Trace, int, []abi.MethodInvocation, error) {
575+
return nil, 0, nil, fmt.Errorf("not implemented")
576+
}

0 commit comments

Comments
 (0)
Please sign in to comment.