diff --git a/api/openapi.json b/api/openapi.json index 40ea5cf6..18025726 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -385,6 +385,32 @@ }, "description": "input parameters for contract get method" }, + "SendBoc": { + "content": { + "application/json": { + "schema": { + "properties": { + "batch": { + "items": { + "example": "te6ccgECBQEAARUAAkWIAWTtae+KgtbrX26Bep8JSq8lFLfGOoyGR/xwdjfvpvEaHg", + "type": "string" + }, + "maxItems": 10, + "minItems": 1, + "type": "array" + }, + "boc": { + "example": "te6ccgECBQEAARUAAkWIAWTtae+KgtbrX26Bep8JSq8lFLfGOoyGR/xwdjfvpvEaHg", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "bag-of-cells serialized to base64", + "required": true + }, "TonConnectProof": { "content": { "application/json": { @@ -4137,7 +4163,7 @@ "description": "Send message to blockchain", "operationId": "sendMessage", "requestBody": { - "$ref": "#/components/requestBodies/Boc" + "$ref": "#/components/requestBodies/SendBoc" }, "responses": { "200": { diff --git a/api/openapi.yml b/api/openapi.yml index 87600f13..df28709f 100644 --- a/api/openapi.yml +++ b/api/openapi.yml @@ -207,7 +207,7 @@ paths: tags: - Blockchain requestBody: - $ref: "#/components/requestBodies/Boc" + $ref: "#/components/requestBodies/SendBoc" responses: '200': description: "success" @@ -2162,6 +2162,24 @@ components: application/json: schema: type: object + SendBoc: + description: bag-of-cells serialized to base64 + required: true + content: + application/json: + schema: + type: object + properties: + boc: + type: string + example: te6ccgECBQEAARUAAkWIAWTtae+KgtbrX26Bep8JSq8lFLfGOoyGR/xwdjfvpvEaHg + batch: + type: array + maxItems: 10 + minItems: 1 + items: + type: string + example: te6ccgECBQEAARUAAkWIAWTtae+KgtbrX26Bep8JSq8lFLfGOoyGR/xwdjfvpvEaHg Boc: description: bag-of-cells serialized to base64 required: true diff --git a/client/oas_client_gen.go b/client/oas_client_gen.go index 58638046..50a1c1bb 100644 --- a/client/oas_client_gen.go +++ b/client/oas_client_gen.go @@ -6534,6 +6534,15 @@ func (c *Client) sendSendMessage(ctx context.Context, request *SendMessageReq) ( otelAttrs := []attribute.KeyValue{ otelogen.OperationID("sendMessage"), } + // Validate request before sending. + if err := func() error { + if err := request.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } // Run stopwatch. startTime := time.Now() diff --git a/client/oas_json_gen.go b/client/oas_json_gen.go index 0656ff3b..e539b16a 100644 --- a/client/oas_json_gen.go +++ b/client/oas_json_gen.go @@ -17017,13 +17017,26 @@ func (s *SendMessageReq) Encode(e *jx.Encoder) { // encodeFields encodes fields. func (s *SendMessageReq) encodeFields(e *jx.Encoder) { { - e.FieldStart("boc") - e.Str(s.Boc) + if s.Boc.Set { + e.FieldStart("boc") + s.Boc.Encode(e) + } + } + { + if s.Batch != nil { + e.FieldStart("batch") + e.ArrStart() + for _, elem := range s.Batch { + e.Str(elem) + } + e.ArrEnd() + } } } -var jsonFieldsNameOfSendMessageReq = [1]string{ +var jsonFieldsNameOfSendMessageReq = [2]string{ 0: "boc", + 1: "batch", } // Decode decodes SendMessageReq from json. @@ -17031,22 +17044,38 @@ func (s *SendMessageReq) Decode(d *jx.Decoder) error { if s == nil { return errors.New("invalid: unable to decode SendMessageReq to nil") } - var requiredBitSet [1]uint8 if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { switch string(k) { case "boc": - requiredBitSet[0] |= 1 << 0 if err := func() error { - v, err := d.Str() - s.Boc = string(v) - if err != nil { + s.Boc.Reset() + if err := s.Boc.Decode(d); err != nil { return err } return nil }(); err != nil { return errors.Wrap(err, "decode field \"boc\"") } + case "batch": + if err := func() error { + s.Batch = make([]string, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem string + v, err := d.Str() + elem = string(v) + if err != nil { + return err + } + s.Batch = append(s.Batch, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"batch\"") + } default: return d.Skip() } @@ -17054,38 +17083,6 @@ func (s *SendMessageReq) Decode(d *jx.Decoder) error { }); err != nil { return errors.Wrap(err, "decode SendMessageReq") } - // Validate required fields. - var failures []validate.FieldError - for i, mask := range [1]uint8{ - 0b00000001, - } { - if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { - // Mask only required fields and check equality to mask using XOR. - // - // If XOR result is not zero, result is not equal to expected, so some fields are missed. - // Bits of fields which would be set are actually bits of missed fields. - missed := bits.OnesCount8(result) - for bitN := 0; bitN < missed; bitN++ { - bitIdx := bits.TrailingZeros8(result) - fieldIdx := i*8 + bitIdx - var name string - if fieldIdx < len(jsonFieldsNameOfSendMessageReq) { - name = jsonFieldsNameOfSendMessageReq[fieldIdx] - } else { - name = strconv.Itoa(fieldIdx) - } - failures = append(failures, validate.FieldError{ - Name: name, - Error: validate.ErrFieldRequired, - }) - // Reset bit. - result &^= 1 << bitIdx - } - } - } - if len(failures) > 0 { - return &validate.Error{Fields: failures} - } return nil } diff --git a/client/oas_schemas_gen.go b/client/oas_schemas_gen.go index 56cca5d8..581b6886 100644 --- a/client/oas_schemas_gen.go +++ b/client/oas_schemas_gen.go @@ -7216,19 +7216,30 @@ func (s *SendMessageLiteServerReq) SetBody(val string) { type SendMessageOK struct{} type SendMessageReq struct { - Boc string `json:"boc"` + Boc OptString `json:"boc"` + Batch []string `json:"batch"` } // GetBoc returns the value of Boc. -func (s *SendMessageReq) GetBoc() string { +func (s *SendMessageReq) GetBoc() OptString { return s.Boc } +// GetBatch returns the value of Batch. +func (s *SendMessageReq) GetBatch() []string { + return s.Batch +} + // SetBoc sets the value of Boc. -func (s *SendMessageReq) SetBoc(val string) { +func (s *SendMessageReq) SetBoc(val OptString) { s.Boc = val } +// SetBatch sets the value of Batch. +func (s *SendMessageReq) SetBatch(val []string) { + s.Batch = val +} + // Ref: #/components/schemas/Seqno type Seqno struct { Seqno uint32 `json:"seqno"` diff --git a/client/oas_validators_gen.go b/client/oas_validators_gen.go index 19e405a7..9ddcb2c8 100644 --- a/client/oas_validators_gen.go +++ b/client/oas_validators_gen.go @@ -1801,6 +1801,30 @@ func (s *Risk) Validate() error { return nil } +func (s *SendMessageReq) Validate() error { + var failures []validate.FieldError + if err := func() error { + if err := (validate.Array{ + MinLength: 1, + MinLengthSet: true, + MaxLength: 10, + MaxLengthSet: true, + }).ValidateLength(len(s.Batch)); err != nil { + return errors.Wrap(err, "array") + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "batch", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + func (s *SmartContractAction) Validate() error { var failures []validate.FieldError if err := func() error { diff --git a/pkg/api/event_handlers.go b/pkg/api/event_handlers.go index 3dd1ec83..587d20e9 100644 --- a/pkg/api/event_handlers.go +++ b/pkg/api/event_handlers.go @@ -27,15 +27,29 @@ func (h Handler) SendMessage(ctx context.Context, request *oas.SendMessageReq) e if h.msgSender == nil { return toError(http.StatusBadRequest, fmt.Errorf("msg sender is not configured")) } - payload, err := base64.StdEncoding.DecodeString(request.Boc) - if err != nil { - return toError(http.StatusBadRequest, err) + if !request.Boc.IsSet() && len(request.Batch) == 0 { + return toError(http.StatusBadRequest, fmt.Errorf("boc not found")) + } + if request.Boc.IsSet() { + payload, err := sendMessage(ctx, request.Boc.Value, h.msgSender) + if err != nil { + sentry.Send("sending message", sentry.SentryInfoData{"payload": request.Boc}, sentry.LevelError) + return toError(http.StatusInternalServerError, err) + } + go h.addToMempool(payload) } - if err := h.msgSender.SendMessage(ctx, payload); err != nil { - sentry.Send("sending message", sentry.SentryInfoData{"payload": request.Boc}, sentry.LevelError) - return toError(http.StatusInternalServerError, err) + if len(request.Batch) > 0 { + var msgsBoc []string + for _, msgBoc := range request.Batch { + payload, err := sendMessage(ctx, msgBoc, h.msgSender) + if err != nil { + msgsBoc = append(msgsBoc, base64.StdEncoding.EncodeToString(payload)) + continue + } + go h.addToMempool(payload) + } + h.msgSender.MsgsBocAddToMempool(msgsBoc) } - go h.addToMempool(payload) return nil } @@ -415,3 +429,15 @@ func emulatedTreeToTrace(tree *txemulator.TxTree, accounts map[tongo.AccountID]t } return t, nil } + +func sendMessage(ctx context.Context, msgBoc string, msgSender messageSender) ([]byte, error) { + payload, err := base64.StdEncoding.DecodeString(msgBoc) + if err != nil { + return nil, err + } + err = msgSender.SendMessage(ctx, payload) + if err != nil { + return nil, err + } + return payload, nil +} diff --git a/pkg/api/interfaces.go b/pkg/api/interfaces.go index 1ce68aca..fce58c3f 100644 --- a/pkg/api/interfaces.go +++ b/pkg/api/interfaces.go @@ -3,9 +3,10 @@ package api import ( "context" "crypto/ed25519" - "github.com/tonkeeper/tongo/boc" "time" + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo" "github.com/tonkeeper/tongo/abi" "github.com/tonkeeper/tongo/liteclient" @@ -113,8 +114,8 @@ type chainState interface { // messageSender provides a method to send a raw message to the blockchain. type messageSender interface { - // SendMessage sends the given payload(a message) to the blockchain. - SendMessage(ctx context.Context, payload []byte) error + SendMessage(ctx context.Context, payload []byte) error // SendMessage sends the given payload(a message) to the blockchain. + MsgsBocAddToMempool(bocMsgs []string) // MsgsBocAddToMempool sends a list of boc to the cache for later sending } // executor runs any get methods diff --git a/pkg/blockchain/msg_sender.go b/pkg/blockchain/msg_sender.go index 29aa83c0..6943cf01 100644 --- a/pkg/blockchain/msg_sender.go +++ b/pkg/blockchain/msg_sender.go @@ -2,6 +2,9 @@ package blockchain import ( "context" + "encoding/base64" + "sync" + "time" "github.com/tonkeeper/tongo/config" "github.com/tonkeeper/tongo/liteapi" @@ -9,14 +12,19 @@ import ( // MsgSender provides a method to send a message to the blockchain. type MsgSender struct { + mu sync.RWMutex client *liteapi.Client // channels is used to send a copy of payload before sending it to the blockchain. channels []chan []byte + // messages is used as a cache for boc multi-sending + messages map[string]int64 // base64, created unix time } func NewMsgSender(servers []config.LiteServer, channels []chan []byte) (*MsgSender, error) { - var err error - var client *liteapi.Client + var ( + client *liteapi.Client + err error + ) if len(servers) == 0 { client, err = liteapi.NewClientWithDefaultMainnet() } else { @@ -25,7 +33,36 @@ func NewMsgSender(servers []config.LiteServer, channels []chan []byte) (*MsgSend if err != nil { return nil, err } - return &MsgSender{client: client, channels: channels}, nil + msgSender := &MsgSender{ + client: client, + channels: channels, + messages: map[string]int64{}, + } + go func() { + for { + msgSender.sendMsgsFromMempool() + time.Sleep(time.Minute * 10) + } + }() + return msgSender, nil +} + +func (ms *MsgSender) sendMsgsFromMempool() { + now := time.Now().Unix() + + ms.mu.RLock() + defer ms.mu.RUnlock() + + for boc, createdTime := range ms.messages { + payload, err := base64.StdEncoding.DecodeString(boc) + if err != nil || now-createdTime > 10800 { // ttl is 3 hours + delete(ms.messages, boc) + continue + } + if err := ms.SendMessage(context.Background(), payload); err != nil { + continue + } + } } // SendMessage sends the given payload(a message) to the blockchain. @@ -39,3 +76,13 @@ func (ms *MsgSender) SendMessage(ctx context.Context, payload []byte) error { _, err := ms.client.SendMessage(ctx, payload) return err } + +func (ms *MsgSender) MsgsBocAddToMempool(bocMsgs []string) { + now := time.Now().Unix() + ms.mu.RLock() + defer ms.mu.RUnlock() + + for _, boc := range bocMsgs { + ms.messages[boc] = now + } +} diff --git a/pkg/oas/oas_json_gen.go b/pkg/oas/oas_json_gen.go index 35c66ee3..01938b15 100644 --- a/pkg/oas/oas_json_gen.go +++ b/pkg/oas/oas_json_gen.go @@ -17017,13 +17017,26 @@ func (s *SendMessageReq) Encode(e *jx.Encoder) { // encodeFields encodes fields. func (s *SendMessageReq) encodeFields(e *jx.Encoder) { { - e.FieldStart("boc") - e.Str(s.Boc) + if s.Boc.Set { + e.FieldStart("boc") + s.Boc.Encode(e) + } + } + { + if s.Batch != nil { + e.FieldStart("batch") + e.ArrStart() + for _, elem := range s.Batch { + e.Str(elem) + } + e.ArrEnd() + } } } -var jsonFieldsNameOfSendMessageReq = [1]string{ +var jsonFieldsNameOfSendMessageReq = [2]string{ 0: "boc", + 1: "batch", } // Decode decodes SendMessageReq from json. @@ -17031,22 +17044,38 @@ func (s *SendMessageReq) Decode(d *jx.Decoder) error { if s == nil { return errors.New("invalid: unable to decode SendMessageReq to nil") } - var requiredBitSet [1]uint8 if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { switch string(k) { case "boc": - requiredBitSet[0] |= 1 << 0 if err := func() error { - v, err := d.Str() - s.Boc = string(v) - if err != nil { + s.Boc.Reset() + if err := s.Boc.Decode(d); err != nil { return err } return nil }(); err != nil { return errors.Wrap(err, "decode field \"boc\"") } + case "batch": + if err := func() error { + s.Batch = make([]string, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem string + v, err := d.Str() + elem = string(v) + if err != nil { + return err + } + s.Batch = append(s.Batch, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"batch\"") + } default: return d.Skip() } @@ -17054,38 +17083,6 @@ func (s *SendMessageReq) Decode(d *jx.Decoder) error { }); err != nil { return errors.Wrap(err, "decode SendMessageReq") } - // Validate required fields. - var failures []validate.FieldError - for i, mask := range [1]uint8{ - 0b00000001, - } { - if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { - // Mask only required fields and check equality to mask using XOR. - // - // If XOR result is not zero, result is not equal to expected, so some fields are missed. - // Bits of fields which would be set are actually bits of missed fields. - missed := bits.OnesCount8(result) - for bitN := 0; bitN < missed; bitN++ { - bitIdx := bits.TrailingZeros8(result) - fieldIdx := i*8 + bitIdx - var name string - if fieldIdx < len(jsonFieldsNameOfSendMessageReq) { - name = jsonFieldsNameOfSendMessageReq[fieldIdx] - } else { - name = strconv.Itoa(fieldIdx) - } - failures = append(failures, validate.FieldError{ - Name: name, - Error: validate.ErrFieldRequired, - }) - // Reset bit. - result &^= 1 << bitIdx - } - } - } - if len(failures) > 0 { - return &validate.Error{Fields: failures} - } return nil } diff --git a/pkg/oas/oas_request_decoders_gen.go b/pkg/oas/oas_request_decoders_gen.go index 003e725e..c34d58a6 100644 --- a/pkg/oas/oas_request_decoders_gen.go +++ b/pkg/oas/oas_request_decoders_gen.go @@ -551,6 +551,14 @@ func (s *Server) decodeSendMessageRequest(r *http.Request) ( } return req, close, err } + if err := func() error { + if err := request.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return req, close, errors.Wrap(err, "validate") + } return &request, close, nil default: return req, close, validate.InvalidContentType(ct) diff --git a/pkg/oas/oas_schemas_gen.go b/pkg/oas/oas_schemas_gen.go index 866686af..48e0122b 100644 --- a/pkg/oas/oas_schemas_gen.go +++ b/pkg/oas/oas_schemas_gen.go @@ -7216,19 +7216,30 @@ func (s *SendMessageLiteServerReq) SetBody(val string) { type SendMessageOK struct{} type SendMessageReq struct { - Boc string `json:"boc"` + Boc OptString `json:"boc"` + Batch []string `json:"batch"` } // GetBoc returns the value of Boc. -func (s *SendMessageReq) GetBoc() string { +func (s *SendMessageReq) GetBoc() OptString { return s.Boc } +// GetBatch returns the value of Batch. +func (s *SendMessageReq) GetBatch() []string { + return s.Batch +} + // SetBoc sets the value of Boc. -func (s *SendMessageReq) SetBoc(val string) { +func (s *SendMessageReq) SetBoc(val OptString) { s.Boc = val } +// SetBatch sets the value of Batch. +func (s *SendMessageReq) SetBatch(val []string) { + s.Batch = val +} + // Ref: #/components/schemas/Seqno type Seqno struct { Seqno uint32 `json:"seqno"` diff --git a/pkg/oas/oas_validators_gen.go b/pkg/oas/oas_validators_gen.go index 5ebd5fdd..65fdb954 100644 --- a/pkg/oas/oas_validators_gen.go +++ b/pkg/oas/oas_validators_gen.go @@ -1801,6 +1801,30 @@ func (s *Risk) Validate() error { return nil } +func (s *SendMessageReq) Validate() error { + var failures []validate.FieldError + if err := func() error { + if err := (validate.Array{ + MinLength: 1, + MinLengthSet: true, + MaxLength: 10, + MaxLengthSet: true, + }).ValidateLength(len(s.Batch)); err != nil { + return errors.Wrap(err, "array") + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "batch", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + func (s *SmartContractAction) Validate() error { var failures []validate.FieldError if err := func() error {