diff --git a/api/openapi.json b/api/openapi.json index 1b9eaea6..a6a2ea96 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -5950,6 +5950,13 @@ }, "type": "array" }, + "progress": { + "example": 0.5, + "format": "float", + "maximum": 1, + "minimum": 0, + "type": "number" + }, "transaction": { "$ref": "#/components/schemas/Transaction" } diff --git a/api/openapi.yml b/api/openapi.yml index 006b5e0a..fc97478f 100644 --- a/api/openapi.yml +++ b/api/openapi.yml @@ -6913,6 +6913,12 @@ components: emulated: type: boolean example: false + progress: + type: number + format: float + minimum: 0 + maximum: 1 + example: 0.5 MessageConsequences: type: object required: diff --git a/pkg/api/event_converters.go b/pkg/api/event_converters.go index df12f37f..f72fba7d 100644 --- a/pkg/api/event_converters.go +++ b/pkg/api/event_converters.go @@ -48,7 +48,16 @@ func distinctAccounts(skip *tongo.AccountID, book addressBook, accounts ...*tong } func convertTrace(t *core.Trace, book addressBook) oas.Trace { - trace := oas.Trace{Transaction: convertTransaction(t.Transaction, t.AccountInterfaces, book), Interfaces: g.ToStrings(t.AccountInterfaces)} + trace := oas.Trace{ + Transaction: convertTransaction(t.Transaction, t.AccountInterfaces, book), + Interfaces: g.ToStrings(t.AccountInterfaces), + Emulated: oas.OptBool{Set: true, Value: t.Emulated}, + } + + // if root transaction + if t.InMsg.IsExternal() { + trace.SetProgress(oas.OptFloat32{Set: true, Value: t.CalculateProgress()}) + } sort.Slice(t.Children, func(i, j int) bool { if t.Children[i].InMsg == nil || t.Children[j].InMsg == nil { diff --git a/pkg/api/event_handlers.go b/pkg/api/event_handlers.go index 4ab9c5ae..31a77fd0 100644 --- a/pkg/api/event_handlers.go +++ b/pkg/api/event_handlers.go @@ -164,6 +164,17 @@ func (h *Handler) GetTrace(ctx context.Context, params oas.GetTraceParams) (*oas if err != nil { return nil, toError(http.StatusInternalServerError, err) } + if trace.InProgress() { + traceId := trace.Hash.Hex() + traceEmulated, _, _, err := h.storage.GetTraceWithState(ctx, traceId[0:len(traceId)/2]) + if err != nil { + h.logger.Warn("get trace from storage: ", zap.Error(err)) + } + if traceEmulated != nil { + traceEmulated = core.CopyTraceData(ctx, trace, traceEmulated) + trace = traceEmulated + } + } convertedTrace := convertTrace(trace, h.addressBook) if emulated { setRecursiveEmulated(&convertedTrace) @@ -193,6 +204,20 @@ func (h *Handler) GetEvent(ctx context.Context, params oas.GetEventParams) (*oas if err != nil { return nil, toError(http.StatusInternalServerError, err) } + + if trace.InProgress() { + hash := trace.Hash.Hex() + traceEmulated, _, _, err := h.storage.GetTraceWithState(ctx, hash[0:len(hash)/2]) + if err != nil { + h.logger.Warn("get trace from storage: ", zap.Error(err)) + } + if traceEmulated != nil { + // we copy data from finished transactions. for emulated it's provided while emulation + traceEmulated = core.CopyTraceData(ctx, trace, traceEmulated) + trace = traceEmulated + } + } + actions, err := bath.FindActions(ctx, trace, bath.WithInformationSource(h.storage)) if err != nil { return nil, toError(http.StatusInternalServerError, err) @@ -399,10 +424,7 @@ func (h *Handler) EmulateMessageToAccountEvent(ctx context.Context, request *oas if err != nil { return nil, toError(http.StatusBadRequest, err) } - hash, err := c.HashString() - if err != nil { - return nil, toError(http.StatusBadRequest, err) - } + hash := m.Hash(true).Hex() trace, version, _, err := h.storage.GetTraceWithState(ctx, hash) if err != nil { h.logger.Warn("get trace from storage: ", zap.Error(err)) @@ -466,12 +488,13 @@ func (h *Handler) EmulateMessageToEvent(ctx context.Context, request *oas.Emulat if err != nil { return nil, toError(http.StatusBadRequest, err) } + var m tlb.Message + if err := tlb.Unmarshal(c, &m); err != nil { + return nil, toError(http.StatusBadRequest, err) + } trace, prs := h.mempoolEmulate.traces.Get(hash) if !prs { - hs, err := c.HashString() - if err != nil { - return nil, toError(http.StatusBadRequest, err) - } + hs := m.Hash(true).Hex() var version int trace, version, _, err = h.storage.GetTraceWithState(ctx, hs) if err != nil { @@ -482,10 +505,6 @@ func (h *Handler) EmulateMessageToEvent(ctx context.Context, request *oas.Emulat if version > h.tongoVersion { savedEmulatedTraces.WithLabelValues("expired").Inc() } - 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) @@ -541,12 +560,14 @@ func (h *Handler) EmulateMessageToTrace(ctx context.Context, request *oas.Emulat if err != nil { return nil, toError(http.StatusBadRequest, err) } + var m tlb.Message + err = tlb.Unmarshal(c, &m) + if err != nil { + return nil, toError(http.StatusBadRequest, err) + } trace, prs := h.mempoolEmulate.traces.Get(hash) if !prs { - hs, err := c.HashString() - if err != nil { - return nil, toError(http.StatusBadRequest, err) - } + hs := m.Hash(true).Hex() var version int trace, version, _, err = h.storage.GetTraceWithState(ctx, hs) if err != nil { @@ -557,11 +578,6 @@ func (h *Handler) EmulateMessageToTrace(ctx context.Context, request *oas.Emulat if version > h.tongoVersion { savedEmulatedTraces.WithLabelValues("expired").Inc() } - 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) @@ -682,10 +698,7 @@ func (h *Handler) EmulateMessageToWallet(ctx context.Context, request *oas.Emula return nil, toError(http.StatusInternalServerError, err) } - hash, err := msgCell.HashString() - if err != nil { - return nil, toError(http.StatusBadRequest, err) - } + hash := m.Hash(true).Hex() trace, version, _, err := h.storage.GetTraceWithState(ctx, hash) if err != nil { h.logger.Warn("get trace from storage: ", zap.Error(err)) diff --git a/pkg/core/trace.go b/pkg/core/trace.go index bc8bfd67..33b94b21 100644 --- a/pkg/core/trace.go +++ b/pkg/core/trace.go @@ -69,6 +69,12 @@ func (t *Trace) SetAdditionalInfo(info *TraceAdditionalInfo) { t.additionalInfo = info } +func (t *Trace) SetAccountInterfaces(ifaces []abi.ContractInterface) { + t.mu.Lock() + defer t.mu.Unlock() + t.AccountInterfaces = ifaces +} + func (t *TraceAdditionalInfo) MarshalJSON() ([]byte, error) { type Alias struct { JettonMasters map[string]string `json:",omitempty"` @@ -137,6 +143,29 @@ func (t *Trace) countUncompleted() int { return c } +func (t *Trace) CalculateProgress() float32 { + var calculateProgress func(t *Trace) + var finished, all int + calculateProgress = func(t *Trace) { + if t == nil { + return + } + if !t.Emulated { + finished += 1 + } + all += 1 + for _, child := range t.Children { + calculateProgress(child) + } + } + calculateProgress(t) + + if all == 0 { + return 0 + } + return float32(finished) / float32(all) +} + type EmulatedTeleitemNFT struct { Index decimal.Decimal CollectionAddress *tongo.AccountID @@ -210,6 +239,42 @@ func DistinctAccounts(trace *Trace) []tongo.AccountID { return maps.Keys(accounts) } +// CopyTraceData copies additional data and transaction data from one trace to another. +// It copies TraceAdditionalInfo, AccountInterfaces, and the embedded Transaction. +func CopyTraceData(ctx context.Context, fromTrace *Trace, toTrace *Trace) *Trace { + additionalDataByHash := make(map[tongo.Bits256]*TraceAdditionalInfo) + interfacesByHash := make(map[tongo.Bits256][]abi.ContractInterface) + transactionByHash := make(map[tongo.Bits256]Transaction) + + Visit(fromTrace, func(trace *Trace) { + if trace.Hash != (tongo.Bits256{}) { + if additionalInfo := trace.AdditionalInfo(); additionalInfo != nil { + additionalDataByHash[trace.Hash] = additionalInfo + } + if len(trace.AccountInterfaces) > 0 { + interfacesByHash[trace.Hash] = trace.AccountInterfaces + } + transactionByHash[trace.Hash] = trace.Transaction + } + }) + + Visit(toTrace, func(trace *Trace) { + if trace.Hash != (tongo.Bits256{}) { + if additionalInfo, exists := additionalDataByHash[trace.Hash]; exists { + trace.SetAdditionalInfo(additionalInfo) + } + if interfaces, exists := interfacesByHash[trace.Hash]; exists { + trace.SetAccountInterfaces(interfaces) + } + if transaction, exists := transactionByHash[trace.Hash]; exists { + trace.Transaction = transaction + } + } + }) + + return toTrace +} + // CollectAdditionalInfo goes over the whole trace // and populates trace.TraceAdditionalInfo based on information // provided by InformationSource. diff --git a/pkg/oas/oas_json_gen.go b/pkg/oas/oas_json_gen.go index 4dab1c9f..95ae14f9 100644 --- a/pkg/oas/oas_json_gen.go +++ b/pkg/oas/oas_json_gen.go @@ -31781,6 +31781,41 @@ func (s *OptExtraCurrencyTransferAction) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode encodes float32 as json. +func (o OptFloat32) Encode(e *jx.Encoder) { + if !o.Set { + return + } + e.Float32(float32(o.Value)) +} + +// Decode decodes float32 from json. +func (o *OptFloat32) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptFloat32 to nil") + } + o.Set = true + v, err := d.Float32() + if err != nil { + return err + } + o.Value = float32(v) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptFloat32) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptFloat32) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode encodes GetAccountsReq as json. func (o OptGetAccountsReq) Encode(e *jx.Encoder) { if !o.Set { @@ -38904,13 +38939,20 @@ func (s *Trace) encodeFields(e *jx.Encoder) { s.Emulated.Encode(e) } } + { + if s.Progress.Set { + e.FieldStart("progress") + s.Progress.Encode(e) + } + } } -var jsonFieldsNameOfTrace = [4]string{ +var jsonFieldsNameOfTrace = [5]string{ 0: "transaction", 1: "interfaces", 2: "children", 3: "emulated", + 4: "progress", } // Decode decodes Trace from json. @@ -38979,6 +39021,16 @@ func (s *Trace) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "decode field \"emulated\"") } + case "progress": + if err := func() error { + s.Progress.Reset() + if err := s.Progress.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"progress\"") + } default: return d.Skip() } diff --git a/pkg/oas/oas_schemas_gen.go b/pkg/oas/oas_schemas_gen.go index ee029801..b03a1ea9 100644 --- a/pkg/oas/oas_schemas_gen.go +++ b/pkg/oas/oas_schemas_gen.go @@ -12738,6 +12738,52 @@ func (o OptExtraCurrencyTransferAction) Or(d ExtraCurrencyTransferAction) ExtraC return d } +// NewOptFloat32 returns new OptFloat32 with value set to v. +func NewOptFloat32(v float32) OptFloat32 { + return OptFloat32{ + Value: v, + Set: true, + } +} + +// OptFloat32 is optional float32. +type OptFloat32 struct { + Value float32 + Set bool +} + +// IsSet returns true if OptFloat32 was set. +func (o OptFloat32) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptFloat32) Reset() { + var v float32 + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptFloat32) SetTo(v float32) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptFloat32) Get() (v float32, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptFloat32) Or(d float32) float32 { + if v, ok := o.Get(); ok { + return v + } + return d +} + // NewOptGetAccountsReq returns new OptGetAccountsReq with value set to v. func NewOptGetAccountsReq(v GetAccountsReq) OptGetAccountsReq { return OptGetAccountsReq{ @@ -16673,6 +16719,7 @@ type Trace struct { Interfaces []string `json:"interfaces"` Children []Trace `json:"children"` Emulated OptBool `json:"emulated"` + Progress OptFloat32 `json:"progress"` } // GetTransaction returns the value of Transaction. @@ -16695,6 +16742,11 @@ func (s *Trace) GetEmulated() OptBool { return s.Emulated } +// GetProgress returns the value of Progress. +func (s *Trace) GetProgress() OptFloat32 { + return s.Progress +} + // SetTransaction sets the value of Transaction. func (s *Trace) SetTransaction(val Transaction) { s.Transaction = val @@ -16715,6 +16767,11 @@ func (s *Trace) SetEmulated(val OptBool) { s.Emulated = val } +// SetProgress sets the value of Progress. +func (s *Trace) SetProgress(val OptFloat32) { + s.Progress = val +} + // Ref: #/components/schemas/TraceID type TraceID struct { ID string `json:"id"` diff --git a/pkg/oas/oas_validators_gen.go b/pkg/oas/oas_validators_gen.go index 06d63a1d..03c293fd 100644 --- a/pkg/oas/oas_validators_gen.go +++ b/pkg/oas/oas_validators_gen.go @@ -4978,6 +4978,33 @@ func (s *Trace) Validate() error { Error: err, }) } + if err := func() error { + if value, ok := s.Progress.Get(); ok { + if err := func() error { + if err := (validate.Float{ + MinSet: true, + Min: 0, + MaxSet: true, + Max: 1, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: nil, + }).Validate(float64(value)); err != nil { + return errors.Wrap(err, "float") + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "progress", + Error: err, + }) + } if len(failures) > 0 { return &validate.Error{Fields: failures} }