|
| 1 | +package api |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/base64" |
| 6 | + "errors" |
| 7 | + "math/big" |
| 8 | + "time" |
| 9 | + |
| 10 | + "github.com/prometheus/client_golang/prometheus" |
| 11 | + "github.com/prometheus/client_golang/prometheus/promauto" |
| 12 | + "github.com/shopspring/decimal" |
| 13 | + "github.com/tonkeeper/tongo" |
| 14 | + "github.com/tonkeeper/tongo/abi" |
| 15 | + "github.com/tonkeeper/tongo/boc" |
| 16 | + "github.com/tonkeeper/tongo/tlb" |
| 17 | + "github.com/tonkeeper/tongo/ton" |
| 18 | + "github.com/tonkeeper/tongo/txemulator" |
| 19 | + tongoWallet "github.com/tonkeeper/tongo/wallet" |
| 20 | + |
| 21 | + "github.com/tonkeeper/opentonapi/pkg/blockchain" |
| 22 | + "github.com/tonkeeper/opentonapi/pkg/cache" |
| 23 | + "github.com/tonkeeper/opentonapi/pkg/core" |
| 24 | + "github.com/tonkeeper/opentonapi/pkg/sentry" |
| 25 | +) |
| 26 | + |
| 27 | +var ( |
| 28 | + emulatedMessagesCounter = promauto.NewCounter(prometheus.CounterOpts{ |
| 29 | + Name: "tonapi_emulated_messages_counter", |
| 30 | + Help: "The total number of emulated messages", |
| 31 | + }) |
| 32 | +) |
| 33 | + |
| 34 | +func (h *Handler) RunEmulation(ctx context.Context, msgCh <-chan blockchain.ExtInMsgCopy, emulationCh chan<- blockchain.ExtInMsgCopy) { |
| 35 | + for { |
| 36 | + select { |
| 37 | + case <-ctx.Done(): |
| 38 | + return |
| 39 | + case msgCopy := <-msgCh: |
| 40 | + emulatedMessagesCounter.Inc() |
| 41 | + go func() { |
| 42 | + ctx, cancel := context.WithTimeout(ctx, time.Second*5) |
| 43 | + defer cancel() |
| 44 | + |
| 45 | + // TODO: find a way to emulate when tonapi receives a batch of messages in a single request to SendBlockchainMessage endpoint. |
| 46 | + _, err := h.addToMempool(ctx, msgCopy.Payload, nil, emulationCh) |
| 47 | + if err != nil { |
| 48 | + sentry.Send("addToMempool", sentry.SentryInfoData{"payload": msgCopy.Payload}, sentry.LevelError) |
| 49 | + } |
| 50 | + }() |
| 51 | + } |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +func (h *Handler) addToMempool(ctx context.Context, bytesBoc []byte, shardAccount map[tongo.AccountID]tlb.ShardAccount, emulationCh chan<- blockchain.ExtInMsgCopy) (map[tongo.AccountID]tlb.ShardAccount, error) { |
| 56 | + if shardAccount == nil { |
| 57 | + shardAccount = map[tongo.AccountID]tlb.ShardAccount{} |
| 58 | + } |
| 59 | + msgCell, err := boc.DeserializeBoc(bytesBoc) |
| 60 | + if err != nil { |
| 61 | + return shardAccount, err |
| 62 | + } |
| 63 | + ttl := int64(30) |
| 64 | + msgV4, err := tongoWallet.DecodeMessageV4(msgCell[0]) |
| 65 | + if err == nil { |
| 66 | + diff := int64(msgV4.ValidUntil) - time.Now().Unix() |
| 67 | + if diff < 600 { |
| 68 | + ttl = diff |
| 69 | + } |
| 70 | + } |
| 71 | + var message tlb.Message |
| 72 | + err = tlb.Unmarshal(msgCell[0], &message) |
| 73 | + if err != nil { |
| 74 | + return shardAccount, err |
| 75 | + } |
| 76 | + config, err := h.storage.TrimmedConfigBase64() |
| 77 | + if err != nil { |
| 78 | + return shardAccount, err |
| 79 | + } |
| 80 | + emulator, err := txemulator.NewTraceBuilder(txemulator.WithAccountsSource(h.storage), |
| 81 | + txemulator.WithAccountsMap(shardAccount), |
| 82 | + txemulator.WithConfigBase64(config), |
| 83 | + txemulator.WithSignatureCheck(), |
| 84 | + ) |
| 85 | + if err != nil { |
| 86 | + return shardAccount, err |
| 87 | + } |
| 88 | + tree, err := emulator.Run(ctx, message) |
| 89 | + if err != nil { |
| 90 | + return shardAccount, err |
| 91 | + } |
| 92 | + newShardAccount := emulator.FinalStates() |
| 93 | + trace, err := emulatedTreeToTrace(ctx, h.executor, h.storage, config, tree, newShardAccount) |
| 94 | + if err != nil { |
| 95 | + return shardAccount, err |
| 96 | + } |
| 97 | + accounts := make(map[tongo.AccountID]struct{}) |
| 98 | + core.Visit(trace, func(node *core.Trace) { |
| 99 | + accounts[node.Account] = struct{}{} |
| 100 | + }) |
| 101 | + hash, err := msgCell[0].Hash256() |
| 102 | + if err != nil { |
| 103 | + return shardAccount, err |
| 104 | + } |
| 105 | + h.mempoolEmulate.traces.Set(hash, trace, cache.WithExpiration(time.Second*time.Duration(ttl))) |
| 106 | + for account := range accounts { |
| 107 | + if _, ok := h.mempoolEmulateIgnoreAccounts[account]; ok { // the map is filled only once at the start |
| 108 | + continue |
| 109 | + } |
| 110 | + oldMemHashes, _ := h.mempoolEmulate.accountsTraces.Get(account) |
| 111 | + newMemHashes := make([]ton.Bits256, 0, len(oldMemHashes)+1) |
| 112 | + for _, mHash := range oldMemHashes { //we need to filter messages which already created transactions |
| 113 | + _, err = h.storage.SearchTransactionByMessageHash(ctx, mHash) |
| 114 | + _, prs := h.mempoolEmulate.traces.Get(mHash) |
| 115 | + if err != nil || prs { //because if err is not null it already happened and if !prs it is not in mempool |
| 116 | + newMemHashes = append(newMemHashes, mHash) |
| 117 | + } |
| 118 | + } |
| 119 | + newMemHashes = append(newMemHashes, hash) // it's important to make it las |
| 120 | + h.mempoolEmulate.accountsTraces.Set(account, newMemHashes, cache.WithExpiration(time.Second*time.Duration(ttl))) |
| 121 | + } |
| 122 | + emulationCh <- blockchain.ExtInMsgCopy{ |
| 123 | + MsgBoc: base64.StdEncoding.EncodeToString(bytesBoc), |
| 124 | + Details: h.ctxToDetails(ctx), |
| 125 | + Payload: bytesBoc, |
| 126 | + Accounts: accounts, |
| 127 | + } |
| 128 | + return newShardAccount, nil |
| 129 | +} |
| 130 | + |
| 131 | +func emulatedTreeToTrace(ctx context.Context, executor executor, resolver core.LibraryResolver, configBase64 string, tree *txemulator.TxTree, accounts map[tongo.AccountID]tlb.ShardAccount) (*core.Trace, error) { |
| 132 | + if !tree.TX.Msgs.InMsg.Exists { |
| 133 | + return nil, errors.New("there is no incoming message in emulation result") |
| 134 | + } |
| 135 | + m := tree.TX.Msgs.InMsg.Value.Value |
| 136 | + var a tlb.MsgAddress |
| 137 | + switch m.Info.SumType { |
| 138 | + case "IntMsgInfo": |
| 139 | + a = m.Info.IntMsgInfo.Dest |
| 140 | + case "ExtInMsgInfo": |
| 141 | + a = m.Info.ExtInMsgInfo.Dest |
| 142 | + default: |
| 143 | + return nil, errors.New("unknown message type in emulation result") |
| 144 | + } |
| 145 | + transaction, err := core.ConvertTransaction(int32(a.AddrStd.WorkchainId), tongo.Transaction{ |
| 146 | + Transaction: tree.TX, |
| 147 | + BlockID: tongo.BlockIDExt{BlockID: tongo.BlockID{Workchain: int32(a.AddrStd.WorkchainId)}}, |
| 148 | + }) |
| 149 | + filteredMsgs := make([]core.Message, 0, len(transaction.OutMsgs)) |
| 150 | + for _, msg := range transaction.OutMsgs { |
| 151 | + if msg.Destination == nil { |
| 152 | + filteredMsgs = append(filteredMsgs, msg) |
| 153 | + } |
| 154 | + } |
| 155 | + transaction.OutMsgs = filteredMsgs //all internal messages in emulation result are delivered to another account and created transaction |
| 156 | + if err != nil { |
| 157 | + return nil, err |
| 158 | + } |
| 159 | + t := &core.Trace{ |
| 160 | + Transaction: *transaction, |
| 161 | + } |
| 162 | + additionalInfo := &core.TraceAdditionalInfo{} |
| 163 | + for i := range tree.Children { |
| 164 | + child, err := emulatedTreeToTrace(ctx, executor, resolver, configBase64, tree.Children[i], accounts) |
| 165 | + if err != nil { |
| 166 | + return nil, err |
| 167 | + } |
| 168 | + t.Children = append(t.Children, child) |
| 169 | + } |
| 170 | + accountID := t.Account |
| 171 | + code := accountCode(accounts[accountID]) |
| 172 | + if code == nil { |
| 173 | + return t, nil |
| 174 | + } |
| 175 | + b, err := code.ToBoc() |
| 176 | + if err != nil { |
| 177 | + return nil, err |
| 178 | + } |
| 179 | + sharedExecutor := newSharedAccountExecutor(accounts, executor, resolver, configBase64) |
| 180 | + inspectionResult, err := abi.NewContractInspector().InspectContract(ctx, b, sharedExecutor, accountID) |
| 181 | + if err != nil { |
| 182 | + return nil, err |
| 183 | + } |
| 184 | + implemented := make(map[abi.ContractInterface]struct{}, len(inspectionResult.ContractInterfaces)) |
| 185 | + for _, iface := range inspectionResult.ContractInterfaces { |
| 186 | + implemented[iface] = struct{}{} |
| 187 | + } |
| 188 | + // TODO: for all obtained Jetton Masters confirm that jetton wallets are valid |
| 189 | + t.AccountInterfaces = inspectionResult.ContractInterfaces |
| 190 | + for _, m := range inspectionResult.GetMethods { |
| 191 | + switch data := m.Result.(type) { |
| 192 | + case abi.GetNftDataResult: |
| 193 | + if _, ok := implemented[abi.Teleitem]; !ok { |
| 194 | + continue |
| 195 | + } |
| 196 | + value := big.Int(data.Index) |
| 197 | + index := decimal.NewFromBigInt(&value, 0) |
| 198 | + collectionAddr, err := tongo.AccountIDFromTlb(data.CollectionAddress) |
| 199 | + if err != nil || collectionAddr == nil { |
| 200 | + continue |
| 201 | + } |
| 202 | + _, nftByIndex, err := abi.GetNftAddressByIndex(ctx, sharedExecutor, *collectionAddr, data.Index) |
| 203 | + if err != nil { |
| 204 | + continue |
| 205 | + } |
| 206 | + indexResult, ok := nftByIndex.(abi.GetNftAddressByIndexResult) |
| 207 | + if !ok { |
| 208 | + continue |
| 209 | + } |
| 210 | + nftAddr, err := tongo.AccountIDFromTlb(indexResult.Address) |
| 211 | + if err != nil || nftAddr == nil { |
| 212 | + continue |
| 213 | + } |
| 214 | + additionalInfo.EmulatedTeleitemNFT = &core.EmulatedTeleitemNFT{ |
| 215 | + Index: index, |
| 216 | + CollectionAddress: collectionAddr, |
| 217 | + Verified: *nftAddr == accountID, |
| 218 | + } |
| 219 | + case abi.GetWalletDataResult: |
| 220 | + master, _ := tongo.AccountIDFromTlb(data.Jetton) |
| 221 | + additionalInfo.SetJettonMaster(accountID, *master) |
| 222 | + case abi.GetSaleData_GetgemsResult: |
| 223 | + price := big.Int(data.FullPrice) |
| 224 | + owner, err := tongo.AccountIDFromTlb(data.Owner) |
| 225 | + if err != nil { |
| 226 | + continue |
| 227 | + } |
| 228 | + item, err := tongo.AccountIDFromTlb(data.Nft) |
| 229 | + if err != nil || item == nil { |
| 230 | + continue |
| 231 | + } |
| 232 | + additionalInfo.NftSaleContract = &core.NftSaleContract{ |
| 233 | + NftPrice: price.Int64(), |
| 234 | + Owner: owner, |
| 235 | + Item: *item, |
| 236 | + } |
| 237 | + case abi.GetSaleData_BasicResult: |
| 238 | + price := big.Int(data.FullPrice) |
| 239 | + owner, err := tongo.AccountIDFromTlb(data.Owner) |
| 240 | + if err != nil { |
| 241 | + continue |
| 242 | + } |
| 243 | + item, err := tongo.AccountIDFromTlb(data.Nft) |
| 244 | + if err != nil || item == nil { |
| 245 | + continue |
| 246 | + } |
| 247 | + additionalInfo.NftSaleContract = &core.NftSaleContract{ |
| 248 | + NftPrice: price.Int64(), |
| 249 | + Owner: owner, |
| 250 | + Item: *item, |
| 251 | + } |
| 252 | + case abi.GetSaleData_GetgemsAuctionResult: |
| 253 | + owner, err := tongo.AccountIDFromTlb(data.Owner) |
| 254 | + if err != nil { |
| 255 | + continue |
| 256 | + } |
| 257 | + item, err := tongo.AccountIDFromTlb(data.Nft) |
| 258 | + if err != nil || item == nil { |
| 259 | + continue |
| 260 | + } |
| 261 | + additionalInfo.NftSaleContract = &core.NftSaleContract{ |
| 262 | + NftPrice: int64(data.MaxBid), |
| 263 | + Owner: owner, |
| 264 | + Item: *item, |
| 265 | + } |
| 266 | + case abi.GetPoolData_StonfiResult: |
| 267 | + t0, err0 := tongo.AccountIDFromTlb(data.Token0Address) |
| 268 | + t1, err1 := tongo.AccountIDFromTlb(data.Token1Address) |
| 269 | + if err1 != nil || err0 != nil { |
| 270 | + continue |
| 271 | + } |
| 272 | + additionalInfo.STONfiPool = &core.STONfiPool{ |
| 273 | + Token0: *t0, |
| 274 | + Token1: *t1, |
| 275 | + } |
| 276 | + for _, accountID := range []ton.AccountID{*t0, *t1} { |
| 277 | + _, value, err := abi.GetWalletData(ctx, sharedExecutor, accountID) |
| 278 | + if err != nil { |
| 279 | + return nil, err |
| 280 | + } |
| 281 | + data := value.(abi.GetWalletDataResult) |
| 282 | + master, _ := tongo.AccountIDFromTlb(data.Jetton) |
| 283 | + additionalInfo.SetJettonMaster(accountID, *master) |
| 284 | + } |
| 285 | + } |
| 286 | + } |
| 287 | + t.SetAdditionalInfo(additionalInfo) |
| 288 | + return t, nil |
| 289 | +} |
0 commit comments