diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1ad760..410d794 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,6 +23,9 @@ jobs: - name: Build run: go build -v . + - name: Test + run: go test ./... + build_docker: name: Docker runs-on: ubuntu-latest diff --git a/.golangci.yml b/.golangci.yml index 4a0ff70..1d172ba 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -14,6 +14,11 @@ linters-settings: check-shadowing: false misspell: locale: US + nlreturn: + # Size of the block (including return statement that is still "OK") + # so no return split required. + # Default: 1 + block-size: 3 staticcheck: checks: ["all"] stylecheck: diff --git a/components/app/app.go b/components/app/app.go index 1d4595a..2c05d67 100644 --- a/components/app/app.go +++ b/components/app/app.go @@ -14,7 +14,7 @@ var ( Name = "inx-mqtt" // Version of the app. - Version = "2.0.0-alpha.3" + Version = "2.0.0-alpha.4" ) func App() *app.App { diff --git a/components/mqtt/component.go b/components/mqtt/component.go index 3d71450..bc58949 100644 --- a/components/mqtt/component.go +++ b/components/mqtt/component.go @@ -7,15 +7,13 @@ import ( "github.com/iotaledger/hive.go/app" "github.com/iotaledger/hive.go/app/shutdown" + "github.com/iotaledger/hive.go/ierrors" "github.com/iotaledger/inx-app/pkg/nodebridge" + "github.com/iotaledger/inx-mqtt/pkg/broker" "github.com/iotaledger/inx-mqtt/pkg/daemon" "github.com/iotaledger/inx-mqtt/pkg/mqtt" ) -const ( - APIRoute = "mqtt/v2" -) - func init() { Component = &app.Component{ Name: "MQTT", @@ -29,7 +27,7 @@ func init() { type dependencies struct { dig.In NodeBridge *nodebridge.NodeBridge - Server *Server + Server *mqtt.Server } var ( @@ -41,31 +39,40 @@ func provide(c *dig.Container) error { type inDeps struct { dig.In - NodeBridge *nodebridge.NodeBridge + NodeBridge nodebridge.NodeBridge *shutdown.ShutdownHandler } - return c.Provide(func(deps inDeps) (*Server, error) { - return NewServer( + return c.Provide(func(deps inDeps) (*mqtt.Server, error) { + broker, err := broker.NewBroker( + broker.WithBufferSize(ParamsMQTT.BufferSize), + broker.WithBufferBlockSize(ParamsMQTT.BufferBlockSize), + broker.WithMaxTopicSubscriptionsPerClient(ParamsMQTT.Subscriptions.MaxTopicSubscriptionsPerClient), + broker.WithTopicCleanupThresholdCount(ParamsMQTT.Subscriptions.TopicsCleanupThresholdCount), + broker.WithTopicCleanupThresholdRatio(ParamsMQTT.Subscriptions.TopicsCleanupThresholdRatio), + broker.WithWebsocketEnabled(ParamsMQTT.Websocket.Enabled), + broker.WithWebsocketBindAddress(ParamsMQTT.Websocket.BindAddress), + broker.WithTCPEnabled(ParamsMQTT.TCP.Enabled), + broker.WithTCPBindAddress(ParamsMQTT.TCP.BindAddress), + broker.WithTCPAuthEnabled(ParamsMQTT.TCP.Auth.Enabled), + broker.WithTCPAuthPasswordSalt(ParamsMQTT.TCP.Auth.PasswordSalt), + broker.WithTCPAuthUsers(ParamsMQTT.TCP.Auth.Users), + broker.WithTCPTLSEnabled(ParamsMQTT.TCP.TLS.Enabled), + broker.WithTCPTLSCertificatePath(ParamsMQTT.TCP.TLS.CertificatePath), + broker.WithTCPTLSPrivateKeyPath(ParamsMQTT.TCP.TLS.PrivateKeyPath), + ) + if err != nil { + return nil, ierrors.Wrap(err, "failed to create MQTT broker") + } + + return mqtt.NewServer( Component.Logger(), deps.NodeBridge, + broker, deps.ShutdownHandler, - mqtt.WithBufferSize(ParamsMQTT.BufferSize), - mqtt.WithBufferBlockSize(ParamsMQTT.BufferBlockSize), - mqtt.WithMaxTopicSubscriptionsPerClient(ParamsMQTT.Subscriptions.MaxTopicSubscriptionsPerClient), - mqtt.WithTopicCleanupThresholdCount(ParamsMQTT.Subscriptions.TopicsCleanupThresholdCount), - mqtt.WithTopicCleanupThresholdRatio(ParamsMQTT.Subscriptions.TopicsCleanupThresholdRatio), mqtt.WithWebsocketEnabled(ParamsMQTT.Websocket.Enabled), mqtt.WithWebsocketBindAddress(ParamsMQTT.Websocket.BindAddress), mqtt.WithWebsocketAdvertiseAddress(ParamsMQTT.Websocket.AdvertiseAddress), - mqtt.WithTCPEnabled(ParamsMQTT.TCP.Enabled), - mqtt.WithTCPBindAddress(ParamsMQTT.TCP.BindAddress), - mqtt.WithTCPAuthEnabled(ParamsMQTT.TCP.Auth.Enabled), - mqtt.WithTCPAuthPasswordSalt(ParamsMQTT.TCP.Auth.PasswordSalt), - mqtt.WithTCPAuthUsers(ParamsMQTT.TCP.Auth.Users), - mqtt.WithTCPTLSEnabled(ParamsMQTT.TCP.TLS.Enabled), - mqtt.WithTCPTLSCertificatePath(ParamsMQTT.TCP.TLS.CertificatePath), - mqtt.WithTCPTLSPrivateKeyPath(ParamsMQTT.TCP.TLS.PrivateKeyPath), ) }) } diff --git a/components/mqtt/publish.go b/components/mqtt/publish.go deleted file mode 100644 index 7feca08..0000000 --- a/components/mqtt/publish.go +++ /dev/null @@ -1,383 +0,0 @@ -package mqtt - -import ( - "context" - "encoding/json" - "strings" - - inx "github.com/iotaledger/inx/go" - iotago "github.com/iotaledger/iota.go/v4" - "github.com/iotaledger/iota.go/v4/hexutil" -) - -func (s *Server) sendMessageOnTopic(topic string, payload []byte) { - if err := s.MQTTBroker.Send(topic, payload); err != nil { - s.LogWarnf("Failed to send message on topic %s: %s", topic, err) - } -} - -func (s *Server) PublishRawOnTopicIfSubscribed(topic string, payload []byte) { - if s.MQTTBroker.HasSubscribers(topic) { - s.sendMessageOnTopic(topic, payload) - } -} - -func (s *Server) PublishPayloadFuncOnTopicIfSubscribed(topic string, payloadFunc func() interface{}) { - if s.MQTTBroker.HasSubscribers(topic) { - s.PublishOnTopic(topic, payloadFunc()) - } -} - -func (s *Server) PublishOnTopicIfSubscribed(topic string, payload interface{}) { - if s.MQTTBroker.HasSubscribers(topic) { - s.PublishOnTopic(topic, payload) - } -} - -func (s *Server) PublishOnTopic(topic string, payload interface{}) { - jsonPayload, err := json.Marshal(payload) - if err != nil { - return - } - - s.sendMessageOnTopic(topic, jsonPayload) -} - -func (s *Server) PublishRawCommitmentOnTopic(topic string, commitment *iotago.Commitment) { - apiForVersion, err := s.NodeBridge.APIProvider().APIForVersion(commitment.ProtocolVersion) - if err != nil { - return - } - - rawCommitment, err := apiForVersion.Encode(commitment) - if err != nil { - return - } - - s.PublishRawOnTopicIfSubscribed(topic, rawCommitment) -} - -func (s *Server) PublishCommitmentInfoOnTopic(topic string, id iotago.CommitmentID) { - s.PublishOnTopicIfSubscribed(topic, &commitmentInfoPayload{ - CommitmentID: id.ToHex(), - CommitmentIndex: uint64(id.Index()), - }) -} - -func (s *Server) PublishBlock(blk *inx.RawBlock) { - apiProvider := s.NodeBridge.APIProvider() - - block, err := blk.UnwrapBlock(apiProvider) - if err != nil { - return - } - - s.PublishRawOnTopicIfSubscribed(topicBlocks, blk.GetData()) - - basicBlk, isBasicBlk := block.Body.(*iotago.BasicBlockBody) - if !isBasicBlk { - return - } - - switch payload := basicBlk.Payload.(type) { - case *iotago.SignedTransaction: - s.PublishRawOnTopicIfSubscribed(topicBlocksTransaction, blk.GetData()) - - //nolint:gocritic // the type switch is nicer here - switch p := payload.Transaction.Payload.(type) { - case *iotago.TaggedData: - s.PublishRawOnTopicIfSubscribed(topicBlocksTransactionTaggedData, blk.GetData()) - if len(p.Tag) > 0 { - txTaggedDataTagTopic := strings.ReplaceAll(topicBlocksTransactionTaggedDataTag, parameterTag, hexutil.EncodeHex(p.Tag)) - s.PublishRawOnTopicIfSubscribed(txTaggedDataTagTopic, blk.GetData()) - } - } - - case *iotago.TaggedData: - s.PublishRawOnTopicIfSubscribed(topicBlocksTaggedData, blk.GetData()) - if len(payload.Tag) > 0 { - taggedDataTagTopic := strings.ReplaceAll(topicBlocksTaggedDataTag, parameterTag, hexutil.EncodeHex(payload.Tag)) - s.PublishRawOnTopicIfSubscribed(taggedDataTagTopic, blk.GetData()) - } - } -} - -func (s *Server) hasSubscriberForTransactionIncludedBlock(transactionID iotago.TransactionID) bool { - transactionTopic := strings.ReplaceAll(topicTransactionsIncludedBlock, parameterTransactionID, transactionID.ToHex()) - - return s.MQTTBroker.HasSubscribers(transactionTopic) -} - -func (s *Server) PublishTransactionIncludedBlock(transactionID iotago.TransactionID, block *inx.RawBlock) { - transactionTopic := strings.ReplaceAll(topicTransactionsIncludedBlock, parameterTransactionID, transactionID.ToHex()) - s.PublishRawOnTopicIfSubscribed(transactionTopic, block.GetData()) -} - -func (s *Server) PublishBlockMetadata(metadata *inx.BlockMetadata) { - blockID := metadata.GetBlockId().Unwrap().ToHex() - singleBlockTopic := strings.ReplaceAll(topicBlockMetadata, parameterBlockID, blockID) - hasSingleBlockTopicSubscriber := s.MQTTBroker.HasSubscribers(singleBlockTopic) - hasAcceptedBlocksTopicSubscriber := s.MQTTBroker.HasSubscribers(topicBlockMetadataAccepted) - hasConfirmedBlocksTopicSubscriber := s.MQTTBroker.HasSubscribers(topicBlockMetadataConfirmed) - - if !hasSingleBlockTopicSubscriber && !hasConfirmedBlocksTopicSubscriber && !hasAcceptedBlocksTopicSubscriber { - return - } - - response := &blockMetadataPayload{ - BlockID: blockID, - BlockState: metadata.GetBlockState(), - BlockFailureReason: metadata.GetBlockFailureReason(), - TxState: metadata.GetTransactionState(), - TxFailureReason: metadata.GetTransactionFailureReason(), - } - - // Serialize here instead of using publishOnTopic to avoid double JSON marshaling - jsonPayload, err := json.Marshal(response) - if err != nil { - return - } - - if hasSingleBlockTopicSubscriber { - s.sendMessageOnTopic(singleBlockTopic, jsonPayload) - } - if hasConfirmedBlocksTopicSubscriber { - s.sendMessageOnTopic(topicBlockMetadataConfirmed, jsonPayload) - } - if hasAcceptedBlocksTopicSubscriber { - s.sendMessageOnTopic(topicBlockMetadataAccepted, jsonPayload) - } -} - -func payloadForOutput(api iotago.API, ledgerIndex iotago.SlotIndex, output *inx.LedgerOutput, iotaOutput iotago.Output) *outputPayload { - rawOutputJSON, err := api.JSONEncode(iotaOutput) - if err != nil { - return nil - } - - rawRawOutputJSON := json.RawMessage(rawOutputJSON) - outputID := output.GetOutputId().Unwrap() - - return &outputPayload{ - Metadata: &outputMetadataPayload{ - BlockID: output.GetBlockId().Unwrap().ToHex(), - TransactionID: outputID.TransactionID().ToHex(), - Spent: false, - OutputIndex: outputID.Index(), - IncludedSlot: uint64(output.SlotBooked), - IncludedCommitmentID: output.GetCommitmentIdIncluded().Unwrap().ToHex(), - LedgerIndex: uint64(ledgerIndex), - }, - RawOutput: &rawRawOutputJSON, - } -} - -func payloadForSpent(api iotago.API, ledgerIndex iotago.SlotIndex, spent *inx.LedgerSpent, iotaOutput iotago.Output) *outputPayload { - payload := payloadForOutput(api, ledgerIndex, spent.GetOutput(), iotaOutput) - if payload != nil { - payload.Metadata.Spent = true - payload.Metadata.SpentSlot = uint64(spent.SlotSpent) - payload.Metadata.CommitmentIDSpent = spent.GetCommitmentIdSpent().Unwrap().ToHex() - payload.Metadata.TransactionIDSpent = spent.UnwrapTransactionIDSpent().ToHex() - } - - return payload -} - -func (s *Server) PublishOnUnlockConditionTopics(baseTopic string, output iotago.Output, payloadFunc func() interface{}) { - - topicFunc := func(condition unlockCondition, addressString string) string { - topic := strings.ReplaceAll(baseTopic, parameterCondition, string(condition)) - - return strings.ReplaceAll(topic, parameterAddress, addressString) - } - - unlockConditions := output.UnlockConditionSet() - - // this tracks the addresses used by any unlock condition - // so that after checking all conditions we can see if anyone is subscribed to the wildcard - addressesToPublishForAny := make(map[string]struct{}) - - address := unlockConditions.Address() - if address != nil { - addr := address.Address.Bech32(s.NodeBridge.APIProvider().CommittedAPI().ProtocolParameters().Bech32HRP()) - s.PublishPayloadFuncOnTopicIfSubscribed(topicFunc(unlockConditionAddress, addr), payloadFunc) - addressesToPublishForAny[addr] = struct{}{} - } - - storageReturn := unlockConditions.StorageDepositReturn() - if storageReturn != nil { - addr := storageReturn.ReturnAddress.Bech32(s.NodeBridge.APIProvider().CommittedAPI().ProtocolParameters().Bech32HRP()) - s.PublishPayloadFuncOnTopicIfSubscribed(topicFunc(unlockConditionStorageReturn, addr), payloadFunc) - addressesToPublishForAny[addr] = struct{}{} - } - - expiration := unlockConditions.Expiration() - if expiration != nil { - addr := expiration.ReturnAddress.Bech32(s.NodeBridge.APIProvider().CommittedAPI().ProtocolParameters().Bech32HRP()) - s.PublishPayloadFuncOnTopicIfSubscribed(topicFunc(unlockConditionExpiration, addr), payloadFunc) - addressesToPublishForAny[addr] = struct{}{} - } - - stateController := unlockConditions.StateControllerAddress() - if stateController != nil { - addr := stateController.Address.Bech32(s.NodeBridge.APIProvider().CommittedAPI().ProtocolParameters().Bech32HRP()) - s.PublishPayloadFuncOnTopicIfSubscribed(topicFunc(unlockConditionStateController, addr), payloadFunc) - addressesToPublishForAny[addr] = struct{}{} - } - - governor := unlockConditions.GovernorAddress() - if governor != nil { - addr := governor.Address.Bech32(s.NodeBridge.APIProvider().CommittedAPI().ProtocolParameters().Bech32HRP()) - s.PublishPayloadFuncOnTopicIfSubscribed(topicFunc(unlockConditionGovernor, addr), payloadFunc) - addressesToPublishForAny[addr] = struct{}{} - } - - immutableAccount := unlockConditions.ImmutableAccount() - if immutableAccount != nil { - addr := immutableAccount.Address.Bech32(s.NodeBridge.APIProvider().CommittedAPI().ProtocolParameters().Bech32HRP()) - s.PublishPayloadFuncOnTopicIfSubscribed(topicFunc(unlockConditionImmutableAlias, addr), payloadFunc) - addressesToPublishForAny[addr] = struct{}{} - } - - for addr := range addressesToPublishForAny { - s.PublishPayloadFuncOnTopicIfSubscribed(topicFunc(unlockConditionAny, addr), payloadFunc) - } -} - -func (s *Server) PublishOnOutputChainTopics(outputID iotago.OutputID, output iotago.Output, payloadFunc func() interface{}) { - - switch o := output.(type) { - case *iotago.NFTOutput: - nftID := o.NFTID - if nftID.Empty() { - // Use implicit NFTID - nftAddr := iotago.NFTAddressFromOutputID(outputID) - nftID = nftAddr.NFTID() - } - topic := strings.ReplaceAll(topicNFTOutputs, parameterNFTID, nftID.String()) - s.PublishPayloadFuncOnTopicIfSubscribed(topic, payloadFunc) - - case *iotago.AccountOutput: - accountID := o.AccountID - if accountID.Empty() { - // Use implicit AccountID - accountID = iotago.AccountIDFromOutputID(outputID) - } - topic := strings.ReplaceAll(topicAccountOutputs, parameterAccountID, accountID.String()) - s.PublishPayloadFuncOnTopicIfSubscribed(topic, payloadFunc) - - case *iotago.FoundryOutput: - foundryID, err := o.FoundryID() - if err != nil { - return - } - topic := strings.ReplaceAll(topicFoundryOutputs, parameterFoundryID, foundryID.String()) - s.PublishPayloadFuncOnTopicIfSubscribed(topic, payloadFunc) - - default: - } -} - -func (s *Server) PublishOutput(ctx context.Context, ledgerIndex iotago.SlotIndex, output *inx.LedgerOutput, publishOnAllTopics bool) { - api := s.NodeBridge.APIProvider().CommittedAPI() - iotaOutput, err := output.UnwrapOutput(api) - if err != nil { - return - } - - var payload *outputPayload - payloadFunc := func() interface{} { - if payload == nil { - payload = payloadForOutput(api, ledgerIndex, output, iotaOutput) - } - - return payload - } - - outputID := output.GetOutputId().Unwrap() - outputsTopic := strings.ReplaceAll(topicOutputs, parameterOutputID, outputID.ToHex()) - s.PublishPayloadFuncOnTopicIfSubscribed(outputsTopic, payloadFunc) - - if publishOnAllTopics { - // If this is the first output in a transaction (index 0), then check if someone is observing the transaction that generated this output - if outputID.Index() == 0 { - ctxFetch, cancelFetch := context.WithTimeout(ctx, fetchTimeout) - defer cancelFetch() - - transactionID := outputID.TransactionID() - if s.hasSubscriberForTransactionIncludedBlock(transactionID) { - s.fetchAndPublishTransactionInclusionWithBlock(ctxFetch, transactionID, output.GetBlockId().Unwrap()) - } - } - - s.PublishOnOutputChainTopics(outputID, iotaOutput, payloadFunc) - s.PublishOnUnlockConditionTopics(topicOutputsByUnlockConditionAndAddress, iotaOutput, payloadFunc) - } -} - -func (s *Server) PublishSpent(ledgerIndex iotago.SlotIndex, spent *inx.LedgerSpent) { - api := s.NodeBridge.APIProvider().CommittedAPI() - iotaOutput, err := spent.GetOutput().UnwrapOutput(api) - if err != nil { - return - } - - var payload *outputPayload - payloadFunc := func() interface{} { - if payload == nil { - payload = payloadForSpent(api, ledgerIndex, spent, iotaOutput) - } - - return payload - } - - outputsTopic := strings.ReplaceAll(topicOutputs, parameterOutputID, spent.GetOutput().GetOutputId().Unwrap().ToHex()) - s.PublishPayloadFuncOnTopicIfSubscribed(outputsTopic, payloadFunc) - - s.PublishOnUnlockConditionTopics(topicSpentOutputsByUnlockConditionAndAddress, iotaOutput, payloadFunc) -} - -func blockIDFromBlockMetadataTopic(topic string) iotago.BlockID { - if strings.HasPrefix(topic, "block-metadata/") && !strings.HasSuffix(topic, "/referenced") { - blockIDHex := strings.Replace(topic, "block-metadata/", "", 1) - blockID, err := iotago.BlockIDFromHexString(blockIDHex) - if err != nil { - return iotago.EmptyBlockID - } - - return blockID - } - - return iotago.EmptyBlockID -} - -func transactionIDFromTransactionsIncludedBlockTopic(topic string) iotago.TransactionID { - if strings.HasPrefix(topic, "transactions/") && strings.HasSuffix(topic, "/included-block") { - transactionIDHex := strings.Replace(topic, "transactions/", "", 1) - transactionIDHex = strings.Replace(transactionIDHex, "/included-block", "", 1) - - transactionID, err := iotago.TransactionIDFromHexString(transactionIDHex) - if err != nil || len(transactionID) != iotago.TransactionIDLength { - return emptyTransactionID - } - - return transactionID - } - - return emptyTransactionID -} - -func outputIDFromOutputsTopic(topic string) iotago.OutputID { - if strings.HasPrefix(topic, "outputs/") && !strings.HasPrefix(topic, "outputs/unlock") { - outputIDHex := strings.Replace(topic, "outputs/", "", 1) - outputID, err := iotago.OutputIDFromHexString(outputIDHex) - if err != nil { - return emptyOutputID - } - - return outputID - } - - return emptyOutputID -} diff --git a/components/mqtt/server.go b/components/mqtt/server.go deleted file mode 100644 index fc3c5a7..0000000 --- a/components/mqtt/server.go +++ /dev/null @@ -1,496 +0,0 @@ -package mqtt - -import ( - "context" - "io" - "math/rand" - "strings" - "sync" - "time" - - "github.com/pkg/errors" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - "github.com/iotaledger/hive.go/app/shutdown" - "github.com/iotaledger/hive.go/lo" - "github.com/iotaledger/hive.go/logger" - "github.com/iotaledger/hive.go/web/subscriptionmanager" - "github.com/iotaledger/inx-app/pkg/nodebridge" - "github.com/iotaledger/inx-mqtt/pkg/mqtt" - inx "github.com/iotaledger/inx/go" - iotago "github.com/iotaledger/iota.go/v4" -) - -const ( - grpcListenToBlocks = "INX.ListenToBlocks" - grpcListenToAcceptedBlocks = "INX.ListenToAcceptedBlocks" - grpcListenToConfirmedBlocks = "INX.ListenToConfirmedBlocks" - grpcListenToLedgerUpdates = "INX.ListenToLedgerUpdates" -) - -const ( - fetchTimeout = 5 * time.Second -) - -var ( - emptyOutputID = iotago.OutputID{} - emptyTransactionID = iotago.TransactionID{} -) - -type topicSubcription struct { - Count int - CancelFunc func() - Identifier int -} - -type Server struct { - *logger.WrappedLogger - - MQTTBroker *mqtt.Broker - NodeBridge *nodebridge.NodeBridge - shutdownHandler *shutdown.ShutdownHandler - brokerOptions *mqtt.BrokerOptions - - grpcSubscriptionsLock sync.Mutex - grpcSubscriptions map[string]*topicSubcription -} - -func NewServer(log *logger.Logger, - bridge *nodebridge.NodeBridge, - shutdownHandler *shutdown.ShutdownHandler, - brokerOpts ...mqtt.BrokerOption) (*Server, error) { - opts := &mqtt.BrokerOptions{} - opts.ApplyOnDefault(brokerOpts...) - - s := &Server{ - WrappedLogger: logger.NewWrappedLogger(log), - NodeBridge: bridge, - shutdownHandler: shutdownHandler, - brokerOptions: opts, - grpcSubscriptions: make(map[string]*topicSubcription), - } - - return s, nil -} - -func (s *Server) Run(ctx context.Context) { - broker, err := mqtt.NewBroker(s.brokerOptions) - if err != nil { - s.LogErrorfAndExit("failed to create MQTT broker: %s", err.Error()) - } - - // register broker events - unhookBrokerEvents := lo.Batch( - broker.Events().ClientConnected.Hook(func(event *subscriptionmanager.ClientEvent[string]) { - s.onClientConnect(event.ClientID) - }).Unhook, - broker.Events().ClientDisconnected.Hook(func(event *subscriptionmanager.ClientEvent[string]) { - s.onClientDisconnect(event.ClientID) - }).Unhook, - broker.Events().TopicSubscribed.Hook(func(event *subscriptionmanager.ClientTopicEvent[string, string]) { - s.onSubscribeTopic(ctx, event.ClientID, event.Topic) - }).Unhook, - broker.Events().TopicUnsubscribed.Hook(func(event *subscriptionmanager.ClientTopicEvent[string, string]) { - s.onUnsubscribeTopic(event.ClientID, event.Topic) - }).Unhook, - ) - s.MQTTBroker = broker - - if err := broker.Start(); err != nil { - s.LogErrorfAndExit("failed to start MQTT broker: %s", err.Error()) - } - - if s.brokerOptions.WebsocketEnabled { - ctxRegister, cancelRegister := context.WithTimeout(ctx, 5*time.Second) - - s.LogInfo("Registering API route ...") - - advertisedAddress := s.brokerOptions.WebsocketBindAddress - if s.brokerOptions.WebsocketAdvertiseAddress != "" { - advertisedAddress = s.brokerOptions.WebsocketAdvertiseAddress - } - - if err := deps.NodeBridge.RegisterAPIRoute(ctxRegister, APIRoute, advertisedAddress, ""); err != nil { - s.LogErrorfAndExit("failed to register API route via INX: %s", err.Error()) - } - s.LogInfo("Registering API route ... done") - cancelRegister() - } - - // register node bridge events - unhookNodeBridgeEvents := lo.Batch( - s.NodeBridge.Events.LatestCommittedSlotChanged.Hook(func(c *nodebridge.Commitment) { - s.PublishCommitmentInfoOnTopic(topicCommitmentInfoLatest, c.CommitmentID) - s.PublishRawCommitmentOnTopic(topicCommitments, c.Commitment) - }).Unhook, - s.NodeBridge.Events.LatestFinalizedSlotChanged.Hook(func(cID iotago.CommitmentID) { - s.PublishCommitmentInfoOnTopic(topicCommitmentInfoFinalized, cID) - }).Unhook, - ) - - s.LogInfo("Starting MQTT Broker ... done") - <-ctx.Done() - - s.LogInfo("Stopping MQTT Broker ...") - unhookBrokerEvents() - unhookNodeBridgeEvents() - - if s.brokerOptions.WebsocketEnabled { - ctxUnregister, cancelUnregister := context.WithTimeout(context.Background(), 5*time.Second) - defer cancelUnregister() - - s.LogInfo("Removing API route ...") - //nolint:contextcheck // false positive - if err := deps.NodeBridge.UnregisterAPIRoute(ctxUnregister, APIRoute); err != nil { - s.LogErrorf("failed to remove API route via INX: %s", err.Error()) - } - cancelUnregister() - } - - if err := s.MQTTBroker.Stop(); err != nil { - s.LogErrorf("failed to stop MQTT broker: %s", err.Error()) - } - - s.LogInfo("Stopping MQTT Broker ...done") -} - -func (s *Server) onClientConnect(clientID string) { - s.LogDebugf("onClientConnect %s", clientID) -} - -func (s *Server) onClientDisconnect(clientID string) { - s.LogDebugf("onClientDisconnect %s", clientID) -} - -func (s *Server) onSubscribeTopic(ctx context.Context, clientID string, topic string) { - s.LogDebugf("%s subscribed to %s", clientID, topic) - switch topic { - - case topicCommitmentInfoLatest: - go s.publishLatestCommitmentInfoTopic() - case topicCommitmentInfoFinalized: - go s.publishFinalizedCommitmentInfoTopic() - case topicCommitments: - go s.publishLatestCommitmentTopic() - - case topicBlocks, topicBlocksTransaction, topicBlocksTransactionTaggedData, topicBlocksTaggedData: - s.startListenIfNeeded(ctx, grpcListenToBlocks, s.listenToBlocks) - - default: - switch { - case strings.HasPrefix(topic, "block-metadata/"): - s.startListenIfNeeded(ctx, grpcListenToAcceptedBlocks, s.listenToAcceptedBlocks) - s.startListenIfNeeded(ctx, grpcListenToConfirmedBlocks, s.listenToConfirmedBlocks) - - if blockID := blockIDFromBlockMetadataTopic(topic); !blockID.Empty() { - go s.fetchAndPublishBlockMetadata(ctx, blockID) - } - - case strings.HasPrefix(topic, "blocks/") && strings.Contains(topic, "tagged-data"): - s.startListenIfNeeded(ctx, grpcListenToBlocks, s.listenToBlocks) - - case strings.HasPrefix(topic, "outputs/") || strings.HasPrefix(topic, "transactions/"): - s.startListenIfNeeded(ctx, grpcListenToLedgerUpdates, s.listenToLedgerUpdates) - - if transactionID := transactionIDFromTransactionsIncludedBlockTopic(topic); transactionID != emptyTransactionID { - go s.fetchAndPublishTransactionInclusion(ctx, transactionID) - } - if outputID := outputIDFromOutputsTopic(topic); outputID != emptyOutputID { - go s.fetchAndPublishOutput(ctx, outputID) - } - } - } -} - -func (s *Server) onUnsubscribeTopic(clientID string, topic string) { - s.LogDebugf("%s unsubscribed from %s", clientID, topic) - switch topic { - case topicBlocks, topicBlocksTransaction, topicBlocksTransactionTaggedData, topicBlocksTaggedData: - s.stopListenIfNeeded(grpcListenToBlocks) - - default: - switch { - case strings.HasPrefix(topic, "block-metadata/"): - s.stopListenIfNeeded(grpcListenToAcceptedBlocks) - s.stopListenIfNeeded(grpcListenToConfirmedBlocks) - - case strings.HasPrefix(topic, "blocks/") && strings.Contains(topic, "tagged-data"): - s.stopListenIfNeeded(grpcListenToBlocks) - - case strings.HasPrefix(topic, "outputs/") || strings.HasPrefix(topic, "transactions/"): - s.stopListenIfNeeded(grpcListenToLedgerUpdates) - } - } -} - -func (s *Server) stopListenIfNeeded(grpcCall string) { - s.grpcSubscriptionsLock.Lock() - defer s.grpcSubscriptionsLock.Unlock() - - sub, ok := s.grpcSubscriptions[grpcCall] - if ok { - // subscription found - // decrease amount of subscribers - sub.Count-- - - if sub.Count == 0 { - // => no more subscribers => stop listening - sub.CancelFunc() - delete(s.grpcSubscriptions, grpcCall) - } - } -} - -func (s *Server) startListenIfNeeded(ctx context.Context, grpcCall string, listenFunc func(context.Context) error) { - s.grpcSubscriptionsLock.Lock() - defer s.grpcSubscriptionsLock.Unlock() - - sub, ok := s.grpcSubscriptions[grpcCall] - if ok { - // subscription already exists - // => increase count to track subscribers - sub.Count++ - - return - } - - ctxCancel, cancel := context.WithCancel(ctx) - - //nolint:gosec // we do not care about weak random numbers here - subscriptionIdentifier := rand.Int() - s.grpcSubscriptions[grpcCall] = &topicSubcription{ - Count: 1, - CancelFunc: cancel, - Identifier: subscriptionIdentifier, - } - go func() { - s.LogInfof("Listen to %s", grpcCall) - err := listenFunc(ctxCancel) - if err != nil && !errors.Is(err, context.Canceled) { - s.LogErrorf("Finished listen to %s with error: %s", grpcCall, err.Error()) - if status.Code(err) == codes.Unavailable { - s.shutdownHandler.SelfShutdown("INX became unavailable", true) - } - } else { - s.LogInfof("Finished listen to %s", grpcCall) - } - s.grpcSubscriptionsLock.Lock() - sub, ok := s.grpcSubscriptions[grpcCall] - if ok && sub.Identifier == subscriptionIdentifier { - // Only delete if it was not already replaced by a new one. - delete(s.grpcSubscriptions, grpcCall) - } - s.grpcSubscriptionsLock.Unlock() - }() -} - -func (s *Server) listenToBlocks(ctx context.Context) error { - - stream, err := s.NodeBridge.Client().ListenToBlocks(ctx, &inx.NoParams{}) - if err != nil { - return err - } - - for { - block, err := stream.Recv() - if err != nil { - if errors.Is(err, io.EOF) || status.Code(err) == codes.Canceled { - break - } - - return err - } - if ctx.Err() != nil { - break - } - s.PublishBlock(block.GetBlock()) - } - - //nolint:nilerr // false positive - return nil -} - -func (s *Server) listenToAcceptedBlocks(ctx context.Context) error { - - stream, err := s.NodeBridge.Client().ListenToAcceptedBlocks(ctx, &inx.NoParams{}) - if err != nil { - return err - } - - for { - blockMetadata, err := stream.Recv() - if err != nil { - if errors.Is(err, io.EOF) || status.Code(err) == codes.Canceled { - break - } - - return err - } - if ctx.Err() != nil { - break - } - s.PublishBlockMetadata(blockMetadata) - } - - //nolint:nilerr // false positive - return nil -} - -func (s *Server) listenToConfirmedBlocks(ctx context.Context) error { - - stream, err := s.NodeBridge.Client().ListenToConfirmedBlocks(ctx, &inx.NoParams{}) - if err != nil { - return err - } - - for { - blockMetadata, err := stream.Recv() - if err != nil { - if errors.Is(err, io.EOF) || status.Code(err) == codes.Canceled { - break - } - - return err - } - if ctx.Err() != nil { - break - } - s.PublishBlockMetadata(blockMetadata) - } - - //nolint:nilerr // false positive - return nil -} - -func (s *Server) listenToLedgerUpdates(ctx context.Context) error { - stream, err := s.NodeBridge.Client().ListenToLedgerUpdates(ctx, &inx.SlotRangeRequest{}) - if err != nil { - return err - } - - var latestIndex iotago.SlotIndex - for { - payload, err := stream.Recv() - if err != nil { - if errors.Is(err, io.EOF) || status.Code(err) == codes.Canceled { - break - } - - return err - } - if ctx.Err() != nil { - break - } - - switch op := payload.GetOp().(type) { - //nolint:nosnakecase // grpc uses underscores - case *inx.LedgerUpdate_BatchMarker: - if op.BatchMarker.GetMarkerType() == inx.LedgerUpdate_Marker_BEGIN { - latestIndex = iotago.SlotIndex(op.BatchMarker.Slot) - } - - //nolint:nosnakecase // grpc uses underscores - case *inx.LedgerUpdate_Consumed: - s.PublishSpent(latestIndex, op.Consumed) - - //nolint:nosnakecase // grpc uses underscores - case *inx.LedgerUpdate_Created: - s.PublishOutput(ctx, latestIndex, op.Created, true) - } - } - - //nolint:nilerr // false positive - return nil -} - -func (s *Server) publishLatestCommitmentTopic() { - s.LogDebug("publishLatestCommitmentTopic") - latest, err := s.NodeBridge.LatestCommitment() - if err != nil { - s.LogErrorf("failed to retrieve latest commitment: %v", err) - - return - } - - s.PublishRawCommitmentOnTopic(topicCommitmentInfoLatest, latest) -} - -func (s *Server) publishLatestCommitmentInfoTopic() { - s.LogDebug("publishLatestCommitmentInfoTopic") - latest, err := s.NodeBridge.LatestCommitment() - if err != nil { - s.LogErrorf("failed to retrieve latest commitment: %v", err) - - return - } - - id, err := latest.ID() - if err != nil { - s.LogErrorf("failed to retrieve latest commitment: %v", err) - - return - } - - s.PublishCommitmentInfoOnTopic(topicCommitmentInfoLatest, id) - - s.LogDebug("publishLatestCommitmentTopic") - s.PublishRawCommitmentOnTopic(topicCommitmentInfoLatest, latest) -} - -func (s *Server) publishFinalizedCommitmentInfoTopic() { - s.LogDebug("publishFinalizedCommitmentInfoTopic") - finalized := s.NodeBridge.LatestFinalizedCommitmentID() - s.PublishCommitmentInfoOnTopic(topicCommitmentInfoFinalized, finalized) -} - -func (s *Server) fetchAndPublishBlockMetadata(ctx context.Context, blockID iotago.BlockID) { - s.LogDebugf("fetchAndPublishBlockMetadata: %s", blockID.ToHex()) - resp, err := s.NodeBridge.Client().ReadBlockMetadata(ctx, inx.NewBlockId(blockID)) - if err != nil { - s.LogErrorf("failed to retrieve block metadata %s: %v", blockID.ToHex(), err) - - return - } - s.PublishBlockMetadata(resp) -} - -func (s *Server) fetchAndPublishOutput(ctx context.Context, outputID iotago.OutputID) { - s.LogDebugf("fetchAndPublishOutput: %s", outputID.ToHex()) - resp, err := s.NodeBridge.Client().ReadOutput(ctx, inx.NewOutputId(outputID)) - if err != nil { - s.LogErrorf("failed to retrieve output %s: %v", outputID.ToHex(), err) - - return - } - s.PublishOutput(ctx, resp.GetLatestCommitmentId().Unwrap().Index(), resp.GetOutput(), false) -} - -func (s *Server) fetchAndPublishTransactionInclusion(ctx context.Context, transactionID iotago.TransactionID) { - s.LogDebugf("fetchAndPublishTransactionInclusion: %s", transactionID.ToHex()) - outputID := iotago.OutputID{} - copy(outputID[:], transactionID[:]) - - resp, err := s.NodeBridge.Client().ReadOutput(ctx, inx.NewOutputId(outputID)) - if err != nil { - s.LogErrorf("failed to retrieve output of transaction %s :%v", transactionID.ToHex(), err) - - return - } - - ctxFetch, cancelFetch := context.WithTimeout(ctx, fetchTimeout) - defer cancelFetch() - - s.fetchAndPublishTransactionInclusionWithBlock(ctxFetch, transactionID, resp.GetOutput().UnwrapBlockID()) -} - -func (s *Server) fetchAndPublishTransactionInclusionWithBlock(ctx context.Context, transactionID iotago.TransactionID, blockID iotago.BlockID) { - s.LogDebugf("fetchAndPublishTransactionInclusionWithBlock: %s", transactionID.ToHex()) - resp, err := s.NodeBridge.Client().ReadBlock(ctx, inx.NewBlockId(blockID)) - if err != nil { - s.LogErrorf("failed to retrieve block %s :%v", blockID.ToHex(), err) - - return - } - s.PublishTransactionIncludedBlock(transactionID, resp) -} diff --git a/components/mqtt/topics.go b/components/mqtt/topics.go deleted file mode 100644 index 5bb839b..0000000 --- a/components/mqtt/topics.go +++ /dev/null @@ -1,50 +0,0 @@ -package mqtt - -// Topic names. -const ( - parameterBlockID = "{blockId}" - parameterTransactionID = "{transactionId}" - parameterOutputID = "{outputId}" - parameterTag = "{tag}" - parameterNFTID = "{nftId}" - parameterAccountID = "{accountId}" - parameterFoundryID = "{foundryId}" - parameterCondition = "{condition}" - parameterAddress = "{address}" - - topicCommitmentInfoLatest = "commitment-info/latest" // commitmentInfoPayload - topicCommitmentInfoFinalized = "commitment-info/finalized" // commitmentInfoPayload - topicCommitments = "commitments" // iotago.Commitment serialized => []bytes - - topicBlocks = "blocks" // iotago.Block serialized => []bytes - topicBlocksTransaction = "blocks/transaction" // iotago.Block serialized => []bytes - topicBlocksTransactionTaggedData = "blocks/transaction/tagged-data" // iotago.Block serialized => []bytes - topicBlocksTransactionTaggedDataTag = "blocks/transaction/tagged-data/" + parameterTag // iotago.Block serialized => []bytes - topicBlocksTaggedData = "blocks/tagged-data" // iotago.Block serialized => []bytes - topicBlocksTaggedDataTag = "blocks/tagged-data/" + parameterTag // iotago.Block serialized => []bytes - - topicTransactionsIncludedBlock = "transactions/" + parameterTransactionID + "/included-block" // iotago.Block serialized => []bytes - - topicBlockMetadata = "block-metadata/" + parameterBlockID // blockMetadataPayload - topicBlockMetadataAccepted = "block-metadata/accepted" // blockMetadataPayload - topicBlockMetadataConfirmed = "block-metadata/confirmed" // blockMetadataPayload - - topicOutputs = "outputs/" + parameterOutputID // outputPayload - topicNFTOutputs = "outputs/nft/" + parameterNFTID // outputPayload - topicAccountOutputs = "outputs/account/" + parameterAccountID // outputPayload - topicFoundryOutputs = "outputs/foundry/" + parameterFoundryID // outputPayload - topicOutputsByUnlockConditionAndAddress = "outputs/unlock/" + parameterCondition + "/" + parameterAddress // outputPayload - topicSpentOutputsByUnlockConditionAndAddress = "outputs/unlock/" + parameterCondition + "/" + parameterAddress + "/spent" // outputPayload -) - -type unlockCondition string - -const ( - unlockConditionAny unlockCondition = "+" - unlockConditionAddress unlockCondition = "address" - unlockConditionStorageReturn unlockCondition = "storage-return" - unlockConditionExpiration unlockCondition = "expiration" - unlockConditionStateController unlockCondition = "state-controller" - unlockConditionGovernor unlockCondition = "governor" - unlockConditionImmutableAlias unlockCondition = "immutable-alias" -) diff --git a/components/mqtt/types.go b/components/mqtt/types.go deleted file mode 100644 index 8865fa0..0000000 --- a/components/mqtt/types.go +++ /dev/null @@ -1,65 +0,0 @@ -package mqtt - -import ( - "encoding/json" - - inx "github.com/iotaledger/inx/go" -) - -// commitmentInfoPayload defines the payload of the commitment latest and confirmed topics. -type commitmentInfoPayload struct { - // The identifier of commitment. - CommitmentID string `json:"commitmentId"` - // The slot index of the commitment. - CommitmentIndex uint64 `json:"commitmentIndex"` -} - -// blockMetadataPayload defines the payload of the block metadata topic. -type blockMetadataPayload struct { - // The hex encoded block ID of the block. - BlockID string `json:"blockId"` - // The state of the block. - //nolint:nosnakecase // grpc uses underscores - BlockState inx.BlockMetadata_BlockState `json:"blockState,omitempty"` - // The reason why the block failed. - //nolint:nosnakecase // grpc uses underscores - BlockFailureReason inx.BlockMetadata_BlockFailureReason `json:"blockFailureReason,omitempty"` - // The state of the transaction. - //nolint:nosnakecase // grpc uses underscores - TxState inx.BlockMetadata_TransactionState `json:"txState,omitempty"` - // The reason why the transaction failed. - //nolint:nosnakecase // grpc uses underscores - TxFailureReason inx.BlockMetadata_TransactionFailureReason `json:"txFailureReason,omitempty"` -} - -// outputMetadataPayload defines the metadata of an output. -type outputMetadataPayload struct { - // The hex encoded block ID of the block. - BlockID string `json:"blockId"` - // The hex encoded transaction id from which this output originated. - TransactionID string `json:"transactionId"` - // The index of the output. - OutputIndex uint16 `json:"outputIndex"` - // Whether this output is spent. - Spent bool `json:"isSpent"` - // The slot index at which the output was spent. - SpentSlot uint64 `json:"spentSlot,omitempty"` - // The commitment ID at which this output was spent. - CommitmentIDSpent string `json:"commitmentIdSpent,omitempty"` - // The transaction this output was spent with. - TransactionIDSpent string `json:"transactionIdSpent,omitempty"` - // The slot index at which the output was booked. - IncludedSlot uint64 `json:"includedSlot,omitempty"` - // The commitment ID at which this output was booked into the ledger. - IncludedCommitmentID string `json:"includedCommitmentId"` - // The current ledger index of the node. - LedgerIndex uint64 `json:"ledgerIndex"` -} - -// outputPayload defines the payload of the output topics. -type outputPayload struct { - // The metadata of the output. - Metadata *outputMetadataPayload `json:"metadata"` - // The output in its serialized form. - RawOutput *json.RawMessage `json:"output"` -} diff --git a/components/prometheus/component.go b/components/prometheus/component.go index 94b89c5..bcf3e6c 100644 --- a/components/prometheus/component.go +++ b/components/prometheus/component.go @@ -14,8 +14,8 @@ import ( "go.uber.org/dig" "github.com/iotaledger/hive.go/app" - "github.com/iotaledger/inx-mqtt/components/mqtt" "github.com/iotaledger/inx-mqtt/pkg/daemon" + "github.com/iotaledger/inx-mqtt/pkg/mqtt" ) func init() { diff --git a/components/prometheus/metrics.go b/components/prometheus/metrics.go index 9a68dbb..b32be61 100644 --- a/components/prometheus/metrics.go +++ b/components/prometheus/metrics.go @@ -3,7 +3,7 @@ package prometheus import ( "github.com/prometheus/client_golang/prometheus" - "github.com/iotaledger/inx-mqtt/components/mqtt" + "github.com/iotaledger/inx-mqtt/pkg/mqtt" ) var ( diff --git a/go.mod b/go.mod index 2335130..c2eccc2 100644 --- a/go.mod +++ b/go.mod @@ -5,17 +5,20 @@ go 1.21 replace github.com/mochi-co/mqtt => github.com/alexsporn/mqtt v0.0.0-20220909140721-d60c438960a4 require ( - github.com/iotaledger/hive.go/app v0.0.0-20231027195901-620bd7470e42 - github.com/iotaledger/hive.go/lo v0.0.0-20231108050255-98e0fa35e936 - github.com/iotaledger/hive.go/logger v0.0.0-20231027195901-620bd7470e42 - github.com/iotaledger/hive.go/web v0.0.0-20230912172434-dc477e1f5140 - github.com/iotaledger/inx-app v1.0.0-rc.3.0.20231108104504-1445f545de82 - github.com/iotaledger/inx/go v1.0.0-rc.2.0.20231108104322-f301c3573998 - github.com/iotaledger/iota.go/v4 v4.0.0-20231108103955-bf75d703d8aa - github.com/labstack/echo/v4 v4.11.2 + github.com/iotaledger/hive.go/app v0.0.0-20231130122510-e3dddb0214f0 + github.com/iotaledger/hive.go/ierrors v0.0.0-20231130122510-e3dddb0214f0 + github.com/iotaledger/hive.go/lo v0.0.0-20231130122510-e3dddb0214f0 + github.com/iotaledger/hive.go/logger v0.0.0-20231130122510-e3dddb0214f0 + github.com/iotaledger/hive.go/runtime v0.0.0-20231130122510-e3dddb0214f0 + github.com/iotaledger/hive.go/web v0.0.0-20231130122510-e3dddb0214f0 + github.com/iotaledger/inx-app v1.0.0-rc.3.0.20231201123347-1c44b3f24221 + github.com/iotaledger/inx/go v1.0.0-rc.2.0.20231201114846-3bb5c3fd5665 + github.com/iotaledger/iota.go/v4 v4.0.0-20231205153517-c03d459b5887 + github.com/labstack/echo/v4 v4.11.3 github.com/mochi-co/mqtt v1.3.2 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.17.0 + github.com/stretchr/testify v1.8.4 go.uber.org/dig v1.17.1 google.golang.org/grpc v1.59.0 ) @@ -25,9 +28,10 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/eclipse/paho.mqtt.golang v1.4.3 // indirect - github.com/ethereum/go-ethereum v1.13.4 // indirect + github.com/ethereum/go-ethereum v1.13.5 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/felixge/fgprof v0.9.3 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -35,24 +39,22 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-github v17.0.0+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect - github.com/gorilla/websocket v1.5.0 // indirect + github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect + github.com/gorilla/websocket v1.5.1 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect - github.com/holiman/uint256 v1.2.3 // indirect + github.com/holiman/uint256 v1.2.4 // indirect github.com/iancoleman/orderedmap v0.3.0 // indirect - github.com/iotaledger/hive.go/constraints v0.0.0-20231108050255-98e0fa35e936 // indirect - github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231108050255-98e0fa35e936 // indirect - github.com/iotaledger/hive.go/crypto v0.0.0-20231108050255-98e0fa35e936 // indirect - github.com/iotaledger/hive.go/ds v0.0.0-20231108044237-5731e50d3660 // indirect - github.com/iotaledger/hive.go/ierrors v0.0.0-20231108050255-98e0fa35e936 // indirect - github.com/iotaledger/hive.go/runtime v0.0.0-20231108050255-98e0fa35e936 // indirect - github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231108050255-98e0fa35e936 // indirect - github.com/iotaledger/hive.go/stringify v0.0.0-20231108050255-98e0fa35e936 // indirect + github.com/iotaledger/hive.go/constraints v0.0.0-20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/hive.go/crypto v0.0.0-20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/hive.go/ds v0.0.0-20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/hive.go/stringify v0.0.0-20231130122510-e3dddb0214f0 // indirect github.com/knadh/koanf v1.5.0 // indirect github.com/kr/text v0.2.0 // indirect - github.com/labstack/gommon v0.4.0 // indirect + github.com/labstack/gommon v0.4.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect @@ -62,27 +64,29 @@ require ( github.com/mr-tron/base58 v1.2.0 // indirect github.com/pasztorpisti/qs v0.0.0-20171216220353-8d6c33ee906c // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect - github.com/petermattis/goid v0.0.0-20230904192822-1876fd5063bc // indirect + github.com/petermattis/goid v0.0.0-20231126143041-f558c26febf5 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/rs/xid v1.5.0 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect - github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sync v0.4.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect - golang.org/x/time v0.3.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.4.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a2b9ac6..1e75d00 100644 --- a/go.sum +++ b/go.sum @@ -63,16 +63,16 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/ethereum/go-ethereum v1.13.4 h1:25HJnaWVg3q1O7Z62LaaI6S9wVq8QCw3K88g8wEzrcM= -github.com/ethereum/go-ethereum v1.13.4/go.mod h1:I0U5VewuuTzvBtVzKo7b3hJzDhXOUtn9mJW7SsIPB0Q= +github.com/ethereum/go-ethereum v1.13.5 h1:U6TCRciCqZRe4FPXmy1sMGxTfuk8P7u2UoinF3VbaFk= +github.com/ethereum/go-ethereum v1.13.5/go.mod h1:yMTu38GSuyxaYzQMViqNmQ1s3cE84abZexQmTgenWk0= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= -github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= @@ -129,11 +129,11 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= -github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f h1:pDhu5sgp8yJlEF/g6osliIIpF9K4F5jvkULXa4daRDQ= -github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk= +github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= @@ -179,41 +179,41 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= -github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o= -github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= +github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= +github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= -github.com/iotaledger/hive.go/app v0.0.0-20231027195901-620bd7470e42 h1:xAER9M9Uoz2EOWT43E9wmXRe+RmAk8OBSUoboH4Su8M= -github.com/iotaledger/hive.go/app v0.0.0-20231027195901-620bd7470e42/go.mod h1:8ZbIKR84oQd/3iQ5eeT7xpudO9/ytzXP7veIYnk7Orc= -github.com/iotaledger/hive.go/constraints v0.0.0-20231108050255-98e0fa35e936 h1:qkq0Wz+Y3J8QYRLd0fwTgHuur/A3k7d82BxOKSfvk8c= -github.com/iotaledger/hive.go/constraints v0.0.0-20231108050255-98e0fa35e936/go.mod h1:dOBOM2s4se3HcWefPe8sQLUalGXJ8yVXw58oK8jke3s= -github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231108050255-98e0fa35e936 h1:GtsYwcCqRomhMo190TPxBrOzs6YnVmqkmQgT/lJrJRo= -github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231108050255-98e0fa35e936/go.mod h1:CdixkrB7VdQzEDlVuwsxPtsiJL/WXrQgz3PELIqlLko= -github.com/iotaledger/hive.go/crypto v0.0.0-20231108050255-98e0fa35e936 h1:Xeb4w0g0Kv2ZjdCZQqz8oiqAU5qAy8OXG8kGTXSPzuY= -github.com/iotaledger/hive.go/crypto v0.0.0-20231108050255-98e0fa35e936/go.mod h1:OQ9EVTTQT1mkO/16BgwSIyQlAhEg+Cptud/yutevWsI= -github.com/iotaledger/hive.go/ds v0.0.0-20231108044237-5731e50d3660 h1:FPvlnahvCscsTyTPbU7DVTWuntfCGb4hgOHEyg75zQc= -github.com/iotaledger/hive.go/ds v0.0.0-20231108044237-5731e50d3660/go.mod h1:RnYZNMRIXKt/SXVsFA18uUBNDZMQm0h5BykMXjv8/8A= -github.com/iotaledger/hive.go/ierrors v0.0.0-20231108050255-98e0fa35e936 h1:o5S4KUAwToOLXoYYRj9ZgqeDsFv1VRM4+Mni0Tdj2Ck= -github.com/iotaledger/hive.go/ierrors v0.0.0-20231108050255-98e0fa35e936/go.mod h1:HcE8B5lP96enc/OALTb2/rIIi+yOLouRoHOKRclKmC8= -github.com/iotaledger/hive.go/lo v0.0.0-20231108050255-98e0fa35e936 h1:coXPklQ7JgqTXIUXh3b4OHml1VIvI8x7pQsjsES/u/s= -github.com/iotaledger/hive.go/lo v0.0.0-20231108050255-98e0fa35e936/go.mod h1:6Ee7i6b4tuTHuRYnPP8VUb0wr9XFI5qlqtnttBd9jRg= -github.com/iotaledger/hive.go/logger v0.0.0-20231027195901-620bd7470e42 h1:7wjs4t1snBDJ8LOTl+tZhr2ORywSOTgJMppxiIAMA0A= -github.com/iotaledger/hive.go/logger v0.0.0-20231027195901-620bd7470e42/go.mod h1:aBfAfIB2GO/IblhYt5ipCbyeL9bXSNeAwtYVA3hZaHg= -github.com/iotaledger/hive.go/runtime v0.0.0-20231108050255-98e0fa35e936 h1:XbC1fmY87UJ/yMs8U2YqlUdJsqb0Xqj/ZYQKlZ7AUG8= -github.com/iotaledger/hive.go/runtime v0.0.0-20231108050255-98e0fa35e936/go.mod h1:DrZPvUvLarK8C2qb+3H2vdypp/MuhpQmB3iMJbDCr/Q= -github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231108050255-98e0fa35e936 h1:LXhLW2cN9bQYoHQsgmJRb/jiRBRU5s2rLoCNjZfgHdg= -github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231108050255-98e0fa35e936/go.mod h1:FoH3T6yKlZJp8xm8K+zsQiibSynp32v21CpWx8xkek8= -github.com/iotaledger/hive.go/stringify v0.0.0-20231108050255-98e0fa35e936 h1:Y4HgL5gm9S27usg5M2t6wi1BSdCxVorM62lwnpKuMd4= -github.com/iotaledger/hive.go/stringify v0.0.0-20231108050255-98e0fa35e936/go.mod h1:FTo/UWzNYgnQ082GI9QVM9HFDERqf9rw9RivNpqrnTs= -github.com/iotaledger/hive.go/web v0.0.0-20230912172434-dc477e1f5140 h1:VkeWPMafzf6DMMhi9Ef3ZI9LQNIwsKVmuY+Gv5JFRBc= -github.com/iotaledger/hive.go/web v0.0.0-20230912172434-dc477e1f5140/go.mod h1:RVlTa6pmKvwpQZAhVKFaU9vNA13iognNVA63zdE6i6U= -github.com/iotaledger/inx-app v1.0.0-rc.3.0.20231108104504-1445f545de82 h1:FdM1lxUKgENO3oOlF5blVqmjER44mLIHGpavyUOY5JI= -github.com/iotaledger/inx-app v1.0.0-rc.3.0.20231108104504-1445f545de82/go.mod h1:HVxkGPraMDTRudfG9AFN7Ga9gijp6skXB9TKNBc4KgI= -github.com/iotaledger/inx/go v1.0.0-rc.2.0.20231108104322-f301c3573998 h1:KkC0SaWrjSMg897r2DDosJYALFfLadFST3Fvoaxg7hw= -github.com/iotaledger/inx/go v1.0.0-rc.2.0.20231108104322-f301c3573998/go.mod h1:c+lBG3vgt2rgXHeOncK8hMllMwihTAtVbu790NslW2w= -github.com/iotaledger/iota.go/v4 v4.0.0-20231108103955-bf75d703d8aa h1:A2nadmSbmn62f6wtrqvv/TCCF2sDiiwyDnl6brbRo1E= -github.com/iotaledger/iota.go/v4 v4.0.0-20231108103955-bf75d703d8aa/go.mod h1:8iDORW4/e4NztyAGqjW07uSMjbhs7snbxw+81IWOczY= +github.com/iotaledger/hive.go/app v0.0.0-20231130122510-e3dddb0214f0 h1:lB5jffAnqhssIPrMnZoiXttRpfymDLDGsW6vLfx0v94= +github.com/iotaledger/hive.go/app v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:+riYmeLApkLlj4+EpuJpEJAsj/KGfD7cqLGy7oTsPOM= +github.com/iotaledger/hive.go/constraints v0.0.0-20231130122510-e3dddb0214f0 h1:hK4CDteuTMBA/ugMJusUJ7PJweWkf56z2ELNvnGB7fU= +github.com/iotaledger/hive.go/constraints v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:dOBOM2s4se3HcWefPe8sQLUalGXJ8yVXw58oK8jke3s= +github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231130122510-e3dddb0214f0 h1:K62beF6iGVwBrMJZA06dcXpqjlm9ZY21xqwq2oBQhVs= +github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231130122510-e3dddb0214f0/go.mod h1:CdixkrB7VdQzEDlVuwsxPtsiJL/WXrQgz3PELIqlLko= +github.com/iotaledger/hive.go/crypto v0.0.0-20231130122510-e3dddb0214f0 h1:F7H4BePrwtWwFXpRVZedU44zpbFks46pTrEFifBm+00= +github.com/iotaledger/hive.go/crypto v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:OQ9EVTTQT1mkO/16BgwSIyQlAhEg+Cptud/yutevWsI= +github.com/iotaledger/hive.go/ds v0.0.0-20231130122510-e3dddb0214f0 h1:TdO6KwA1uvQKG5uJ2ywDTLPzfXF7xGD3C0l1pWvSvZg= +github.com/iotaledger/hive.go/ds v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:JE8cbZSvzbB5TrwXibg6M0B7ck35YxF30ItHBzQRlgc= +github.com/iotaledger/hive.go/ierrors v0.0.0-20231130122510-e3dddb0214f0 h1:mP2pgiT4NufRBgATE3UuqgLNXBl4YGtWBQt3U8Mh6NU= +github.com/iotaledger/hive.go/ierrors v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:HcE8B5lP96enc/OALTb2/rIIi+yOLouRoHOKRclKmC8= +github.com/iotaledger/hive.go/lo v0.0.0-20231130122510-e3dddb0214f0 h1:DI78+2CRpWBfKzmXlEqJB6o4j0gl4KUIUwHojcdkZuc= +github.com/iotaledger/hive.go/lo v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:6Ee7i6b4tuTHuRYnPP8VUb0wr9XFI5qlqtnttBd9jRg= +github.com/iotaledger/hive.go/logger v0.0.0-20231130122510-e3dddb0214f0 h1:Vgz30mlavKHTGHzjnsYPJ2SPGwbFjUuFm/1KJtfRhyQ= +github.com/iotaledger/hive.go/logger v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:w1psHM2MuKsen1WdsPKrpqElYH7ZOQ+YdQIgJZg4HTo= +github.com/iotaledger/hive.go/runtime v0.0.0-20231130122510-e3dddb0214f0 h1:5YDrqQ8J+MIdICVrOMBwOIdzz6hILgQNLjrzYkC5yoI= +github.com/iotaledger/hive.go/runtime v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:DrZPvUvLarK8C2qb+3H2vdypp/MuhpQmB3iMJbDCr/Q= +github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231130122510-e3dddb0214f0 h1:UE9L3O12daz2pEp5TkFVo0rBvHYUwXWmpoWRJ1KRI0w= +github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231130122510-e3dddb0214f0/go.mod h1:FoH3T6yKlZJp8xm8K+zsQiibSynp32v21CpWx8xkek8= +github.com/iotaledger/hive.go/stringify v0.0.0-20231130122510-e3dddb0214f0 h1:cFJCQh5FBEePoWmc4dTZvCrKeJ+MaSzmP3jpn8VsSzQ= +github.com/iotaledger/hive.go/stringify v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:FTo/UWzNYgnQ082GI9QVM9HFDERqf9rw9RivNpqrnTs= +github.com/iotaledger/hive.go/web v0.0.0-20231130122510-e3dddb0214f0 h1:bIfWp0AOQXMv3VkDAOQwqvMYLz1Ila1Z2hqiY2RJ3Io= +github.com/iotaledger/hive.go/web v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:L/CLz7skt9dvidhBOw2gmMGhmrUBHXlA0b3paugdsE4= +github.com/iotaledger/inx-app v1.0.0-rc.3.0.20231201123347-1c44b3f24221 h1:+ozrau44uPy2kYv2fuj2Wks8+VkXR62WB9zONOJgzdE= +github.com/iotaledger/inx-app v1.0.0-rc.3.0.20231201123347-1c44b3f24221/go.mod h1:6cLX3gnhP0WL+Q+mf3/rIqfACe5fWKVR8luPXWh2xiY= +github.com/iotaledger/inx/go v1.0.0-rc.2.0.20231201114846-3bb5c3fd5665 h1:XdhojOpZ0t0pJFyNO0zlBogSAUrhEI67eCpTC9H6sGM= +github.com/iotaledger/inx/go v1.0.0-rc.2.0.20231201114846-3bb5c3fd5665/go.mod h1:obK1N42oafGA7EH6zC4VX2fKh7jTa3WnIa9h1azfxq0= +github.com/iotaledger/iota.go/v4 v4.0.0-20231205153517-c03d459b5887 h1:i42HEMC0oWjKOy/v53sP45T8EHavhnk6BFpX1TRYUSI= +github.com/iotaledger/iota.go/v4 v4.0.0-20231205153517-c03d459b5887/go.mod h1:lCk9rhP3B5pX9BKhzR+Jobq4xPd+GHlqgF4Ga+eQfWA= github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -241,14 +241,13 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.11.2 h1:T+cTLQxWCDfqDEoydYm5kCobjmHwOwcv4OJAPHilmdE= -github.com/labstack/echo/v4 v4.11.2/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM= +github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= +github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk= +github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -256,7 +255,6 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -301,8 +299,8 @@ github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAv github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= -github.com/petermattis/goid v0.0.0-20230904192822-1876fd5063bc h1:8bQZVK1X6BJR/6nYUPxQEP+ReTsceJTKizeuwjWOPUA= -github.com/petermattis/goid v0.0.0-20230904192822-1876fd5063bc/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/petermattis/goid v0.0.0-20231126143041-f558c26febf5 h1:+qIP3OMrT7SN5kLnTcVEISPOMB/97RyAKTg1UWA738E= +github.com/petermattis/goid v0.0.0-20231126143041-f558c26febf5/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -352,8 +350,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -374,7 +372,6 @@ github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e h1:IWllFTiDjjLIf2 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkTYKSv5m6MCKkOQlHwaShTMl3HjqSGW3XtVhXM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -401,8 +398,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= @@ -432,8 +429,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -446,8 +443,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -478,15 +475,12 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -494,11 +488,11 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY= +golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -524,8 +518,8 @@ google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 h1:AB/lmRny7e2pLhFEYIbl5qkDAUt2h0ZRO4wGPhZf+ik= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= diff --git a/pkg/mqtt/auth.go b/pkg/broker/auth.go similarity index 99% rename from pkg/mqtt/auth.go rename to pkg/broker/auth.go index cd0a272..a8d25de 100644 --- a/pkg/mqtt/auth.go +++ b/pkg/broker/auth.go @@ -1,4 +1,4 @@ -package mqtt +package broker import ( "encoding/hex" diff --git a/pkg/broker/broker.go b/pkg/broker/broker.go new file mode 100644 index 0000000..2a11260 --- /dev/null +++ b/pkg/broker/broker.go @@ -0,0 +1,201 @@ +package broker + +import ( + "crypto/tls" + "errors" + "fmt" + "net" + + mqtt "github.com/mochi-co/mqtt/server" + "github.com/mochi-co/mqtt/server/events" + "github.com/mochi-co/mqtt/server/listeners" + "github.com/mochi-co/mqtt/server/listeners/auth" + "github.com/mochi-co/mqtt/server/system" + + "github.com/iotaledger/hive.go/web/subscriptionmanager" +) + +type Broker interface { + // Events returns the events of the broker. + Events() *subscriptionmanager.Events[string, string] + // Start the broker. + Start() error + // Stop the broker. + Stop() error + // HasSubscribers returns true if the topic has subscribers. + HasSubscribers(topic string) bool + // Send publishes a message. + Send(topic string, payload []byte) error + // SystemInfo returns the metrics of the broker. + SystemInfo() *system.Info + // SubscribersSize returns the size of the underlying map of the SubscriptionManager. + SubscribersSize() int + // TopicsSize returns the size of all underlying maps of the SubscriptionManager. + TopicsSize() int +} + +// Broker is a simple mqtt publisher abstraction. +type broker struct { + server *mqtt.Server + opts *Options + subscriptionManager *subscriptionmanager.SubscriptionManager[string, string] + unhook func() +} + +// NewBroker creates a new broker. +func NewBroker(brokerOpts ...Option) (Broker, error) { + opts := &Options{} + opts.ApplyOnDefault(brokerOpts...) + + if !opts.WebsocketEnabled && !opts.TCPEnabled { + return nil, errors.New("at least websocket or TCP must be enabled") + } + + server := mqtt.NewServer(&mqtt.Options{ + BufferSize: opts.BufferSize, + BufferBlockSize: opts.BufferBlockSize, + InflightTTL: 30, + }) + + if opts.WebsocketEnabled { + // check websocket bind address + _, _, err := net.SplitHostPort(opts.WebsocketBindAddress) + if err != nil { + return nil, fmt.Errorf("parsing websocket bind address (%s) failed: %w", opts.WebsocketBindAddress, err) + } + + ws := listeners.NewWebsocket("ws1", opts.WebsocketBindAddress) + if err := server.AddListener(ws, &listeners.Config{ + Auth: &AuthAllowEveryone{}, + TLS: nil, + }); err != nil { + return nil, fmt.Errorf("adding websocket listener failed: %w", err) + } + } + + if opts.TCPEnabled { + // check tcp bind address + _, _, err := net.SplitHostPort(opts.TCPBindAddress) + if err != nil { + return nil, fmt.Errorf("parsing TCP bind address (%s) failed: %w", opts.TCPBindAddress, err) + } + + tcp := listeners.NewTCP("t1", opts.TCPBindAddress) + + var tcpAuthController auth.Controller + if opts.TCPAuthEnabled { + var err error + tcpAuthController, err = NewAuthAllowUsers(opts.TCPAuthPasswordSalt, opts.TCPAuthUsers) + if err != nil { + return nil, fmt.Errorf("enabling TCP Authentication failed: %w", err) + } + } else { + tcpAuthController = &AuthAllowEveryone{} + } + + var tlsConfig *tls.Config + if opts.TCPTLSEnabled { + var err error + + tlsConfig, err = NewTLSConfig(opts.TCPTLSCertificatePath, opts.TCPTLSPrivateKeyPath) + if err != nil { + return nil, fmt.Errorf("enabling TCP TLS failed: %w", err) + } + } + + if err := server.AddListener(tcp, &listeners.Config{ + Auth: tcpAuthController, + TLSConfig: tlsConfig, + }); err != nil { + return nil, fmt.Errorf("adding TCP listener failed: %w", err) + } + } + + s := subscriptionmanager.New( + subscriptionmanager.WithMaxTopicSubscriptionsPerClient[string, string](opts.MaxTopicSubscriptionsPerClient), + subscriptionmanager.WithCleanupThresholdCount[string, string](opts.TopicCleanupThresholdCount), + subscriptionmanager.WithCleanupThresholdRatio[string, string](opts.TopicCleanupThresholdRatio), + ) + + // this event is used to drop malicious clients + unhook := s.Events().DropClient.Hook(func(event *subscriptionmanager.DropClientEvent[string]) { + client, exists := server.Clients.Get(event.ClientID) + if !exists { + return + } + + // stop the client connection + client.Stop(event.Reason) + + // delete the client from the broker + server.Clients.Delete(event.ClientID) + }).Unhook + + // bind the broker events to the SubscriptionManager to track the subscriptions + server.Events.OnConnect = func(cl events.Client, pk events.Packet) { + s.Connect(cl.ID) + } + + server.Events.OnDisconnect = func(cl events.Client, err error) { + s.Disconnect(cl.ID) + } + + server.Events.OnSubscribe = func(topic string, cl events.Client, qos byte) { + s.Subscribe(cl.ID, topic) + } + + server.Events.OnUnsubscribe = func(topic string, cl events.Client) { + s.Unsubscribe(cl.ID, topic) + } + + return &broker{ + server: server, + opts: opts, + subscriptionManager: s, + unhook: unhook, + }, nil +} + +// Events returns the events of the broker. +func (b *broker) Events() *subscriptionmanager.Events[string, string] { + return b.subscriptionManager.Events() +} + +// Start the broker. +func (b *broker) Start() error { + return b.server.Serve() +} + +// Stop the broker. +func (b *broker) Stop() error { + if b.unhook != nil { + b.unhook() + } + + return b.server.Close() +} + +// SystemInfo returns the metrics of the broker. +func (b *broker) HasSubscribers(topic string) bool { + return b.subscriptionManager.TopicHasSubscribers(topic) +} + +// Send publishes a message. +func (b *broker) Send(topic string, payload []byte) error { + return b.server.Publish(topic, payload, false) +} + +// SystemInfo returns the metrics of the broker. +func (b *broker) SystemInfo() *system.Info { + return b.server.System +} + +// SubscribersSize returns the size of the underlying map of the SubscriptionManager. +func (b *broker) SubscribersSize() int { + return b.subscriptionManager.SubscribersSize() +} + +// TopicsSize returns the size of all underlying maps of the SubscriptionManager. +func (b *broker) TopicsSize() int { + return b.subscriptionManager.TopicsSize() +} diff --git a/pkg/mqtt/broker_options.go b/pkg/broker/options.go similarity index 67% rename from pkg/mqtt/broker_options.go rename to pkg/broker/options.go index de760d6..0f1cad3 100644 --- a/pkg/mqtt/broker_options.go +++ b/pkg/broker/options.go @@ -1,7 +1,7 @@ -package mqtt +package broker -// BrokerOptions are options around the broker. -type BrokerOptions struct { +// Options are options around the broker. +type Options struct { // BufferSize is the size of the client buffers in bytes. BufferSize int // BufferBlockSize is the size per client buffer R/W block in bytes. @@ -18,9 +18,6 @@ type BrokerOptions struct { WebsocketEnabled bool // WebsocketBindAddress defines the websocket bind address on which the MQTT broker listens on. WebsocketBindAddress string - // WebsocketAdvertiseAddress defines the address of the websocket of the MQTT broker which is advertised to the INX Server (optional). - WebsocketAdvertiseAddress string - // TCPEnabled defines whether to enable the TCP connection of the MQTT broker. TCPEnabled bool // TCPBindAddress defines the TCP bind address on which the MQTT broker listens on. @@ -41,14 +38,13 @@ type BrokerOptions struct { TCPTLSPrivateKeyPath string } -var defaultBrokerOpts = []BrokerOption{ +var defaultOpts = []Option{ WithBufferSize(0), WithBufferBlockSize(0), WithTopicCleanupThresholdCount(10000), WithTopicCleanupThresholdRatio(1.0), WithWebsocketEnabled(true), WithWebsocketBindAddress("localhost:1888"), - WithWebsocketAdvertiseAddress(""), WithTCPEnabled(false), WithTCPBindAddress("localhost:1883"), WithTCPAuthEnabled(false), @@ -59,130 +55,123 @@ var defaultBrokerOpts = []BrokerOption{ WithTCPTLSPrivateKeyPath(""), } -// applies the given BrokerOption. -func (bo *BrokerOptions) apply(opts ...BrokerOption) { +// applies the given Option. +func (bo *Options) apply(opts ...Option) { for _, opt := range opts { opt(bo) } } // ApplyOnDefault applies the given options on top of the default options. -func (bo *BrokerOptions) ApplyOnDefault(opts ...BrokerOption) { - bo.apply(defaultBrokerOpts...) +func (bo *Options) ApplyOnDefault(opts ...Option) { + bo.apply(defaultOpts...) bo.apply(opts...) } -// BrokerOption is a function which sets an option on a BrokerOptions instance. -type BrokerOption func(options *BrokerOptions) +// Option is a function which sets an option on a Options instance. +type Option func(options *Options) // WithBufferSize sets the size of the client buffers in bytes. -func WithBufferSize(bufferSize int) BrokerOption { - return func(options *BrokerOptions) { +func WithBufferSize(bufferSize int) Option { + return func(options *Options) { options.BufferSize = bufferSize } } // WithBufferBlockSize sets the size per client buffer R/W block in bytes. -func WithBufferBlockSize(bufferBlockSize int) BrokerOption { - return func(options *BrokerOptions) { +func WithBufferBlockSize(bufferBlockSize int) Option { + return func(options *Options) { options.BufferBlockSize = bufferBlockSize } } // WithMaxTopicSubscriptionsPerClient sets the maximum number of topic subscriptions per client before the client gets dropped (DOS protection). -func WithMaxTopicSubscriptionsPerClient(maxTopicSubscriptionsPerClient int) BrokerOption { - return func(options *BrokerOptions) { +func WithMaxTopicSubscriptionsPerClient(maxTopicSubscriptionsPerClient int) Option { + return func(options *Options) { options.MaxTopicSubscriptionsPerClient = maxTopicSubscriptionsPerClient } } // WithTopicCleanupThresholdCount sets the number of deleted topics that trigger a garbage collection of the SubscriptionManager. -func WithTopicCleanupThresholdCount(topicCleanupThresholdCount int) BrokerOption { - return func(options *BrokerOptions) { +func WithTopicCleanupThresholdCount(topicCleanupThresholdCount int) Option { + return func(options *Options) { options.TopicCleanupThresholdCount = topicCleanupThresholdCount } } // WithTopicCleanupThresholdRatio the ratio of subscribed topics to deleted topics that trigger a garbage collection of the SubscriptionManager. -func WithTopicCleanupThresholdRatio(topicCleanupThresholdRatio float32) BrokerOption { - return func(options *BrokerOptions) { +func WithTopicCleanupThresholdRatio(topicCleanupThresholdRatio float32) Option { + return func(options *Options) { options.TopicCleanupThresholdRatio = topicCleanupThresholdRatio } } // WithWebsocketEnabled sets whether to enable the websocket connection of the MQTT broker. -func WithWebsocketEnabled(websocketEnabled bool) BrokerOption { - return func(options *BrokerOptions) { +func WithWebsocketEnabled(websocketEnabled bool) Option { + return func(options *Options) { options.WebsocketEnabled = websocketEnabled } } // WithWebsocketBindAddress sets the websocket bind address on which the MQTT broker listens on. -func WithWebsocketBindAddress(websocketBindAddress string) BrokerOption { - return func(options *BrokerOptions) { +func WithWebsocketBindAddress(websocketBindAddress string) Option { + return func(options *Options) { options.WebsocketBindAddress = websocketBindAddress } } -// WithWebsocketBindAddress sets the address of the websocket of the MQTT broker which is advertised to the INX Server (optional). -func WithWebsocketAdvertiseAddress(websocketAdvertiseAddress string) BrokerOption { - return func(options *BrokerOptions) { - options.WebsocketAdvertiseAddress = websocketAdvertiseAddress - } -} - // WithTCPEnabled sets whether to enable the TCP connection of the MQTT broker. -func WithTCPEnabled(tcpEnabled bool) BrokerOption { - return func(options *BrokerOptions) { +func WithTCPEnabled(tcpEnabled bool) Option { + return func(options *Options) { options.TCPEnabled = tcpEnabled } } // WithTCPBindAddress sets the TCP bind address on which the MQTT broker listens on. -func WithTCPBindAddress(tcpBindAddress string) BrokerOption { - return func(options *BrokerOptions) { +func WithTCPBindAddress(tcpBindAddress string) Option { + return func(options *Options) { options.TCPBindAddress = tcpBindAddress } } // WithTCPAuthEnabled sets whether to enable auth for TCP connections. -func WithTCPAuthEnabled(tcpAuthEnabled bool) BrokerOption { - return func(options *BrokerOptions) { +func WithTCPAuthEnabled(tcpAuthEnabled bool) Option { + return func(options *Options) { options.TCPAuthEnabled = tcpAuthEnabled } } // WithTCPAuthPasswordSalt sets the auth salt used for hashing the passwords of the users. -func WithTCPAuthPasswordSalt(tcpAuthPasswordSalt string) BrokerOption { - return func(options *BrokerOptions) { +func WithTCPAuthPasswordSalt(tcpAuthPasswordSalt string) Option { + return func(options *Options) { options.TCPAuthPasswordSalt = tcpAuthPasswordSalt } } // WithTCPAuthUsers sets the list of allowed users with their password+salt as a scrypt hash. -func WithTCPAuthUsers(tcpAuthUsers map[string]string) BrokerOption { - return func(options *BrokerOptions) { +func WithTCPAuthUsers(tcpAuthUsers map[string]string) Option { + return func(options *Options) { options.TCPAuthUsers = tcpAuthUsers } } // WithTCPTLSEnabled sets whether to enable TLS for TCP connections. -func WithTCPTLSEnabled(tcpTLSEnabled bool) BrokerOption { - return func(options *BrokerOptions) { +func WithTCPTLSEnabled(tcpTLSEnabled bool) Option { + return func(options *Options) { options.TCPTLSEnabled = tcpTLSEnabled } } // WithTCPTLSCertificatePath sets the path to the certificate file (x509 PEM) for TCP connections with TLS. -func WithTCPTLSCertificatePath(tcpTLSCertificatePath string) BrokerOption { - return func(options *BrokerOptions) { +func WithTCPTLSCertificatePath(tcpTLSCertificatePath string) Option { + return func(options *Options) { options.TCPTLSCertificatePath = tcpTLSCertificatePath } } // WithTCPTLSPrivateKeyPath sets the path to the private key file (x509 PEM) for TCP connections with TLS. -func WithTCPTLSPrivateKeyPath(tcpTLSPrivateKeyPath string) BrokerOption { - return func(options *BrokerOptions) { +func WithTCPTLSPrivateKeyPath(tcpTLSPrivateKeyPath string) Option { + return func(options *Options) { options.TCPTLSPrivateKeyPath = tcpTLSPrivateKeyPath } } diff --git a/pkg/mqtt/tls.go b/pkg/broker/tls.go similarity index 98% rename from pkg/mqtt/tls.go rename to pkg/broker/tls.go index b01a93c..6a5d3f7 100644 --- a/pkg/mqtt/tls.go +++ b/pkg/broker/tls.go @@ -1,4 +1,4 @@ -package mqtt +package broker import ( "crypto/tls" diff --git a/pkg/mqtt/broker.go b/pkg/mqtt/broker.go deleted file mode 100644 index 58a835e..0000000 --- a/pkg/mqtt/broker.go +++ /dev/null @@ -1,178 +0,0 @@ -package mqtt - -import ( - "crypto/tls" - "errors" - "fmt" - "net" - - mqtt "github.com/mochi-co/mqtt/server" - "github.com/mochi-co/mqtt/server/events" - "github.com/mochi-co/mqtt/server/listeners" - "github.com/mochi-co/mqtt/server/listeners/auth" - "github.com/mochi-co/mqtt/server/system" - - "github.com/iotaledger/hive.go/web/subscriptionmanager" -) - -// Broker is a simple mqtt publisher abstraction. -type Broker struct { - broker *mqtt.Server - opts *BrokerOptions - subscriptionManager *subscriptionmanager.SubscriptionManager[string, string] - unhook func() -} - -// NewBroker creates a new broker. -func NewBroker(brokerOpts *BrokerOptions) (*Broker, error) { - - if !brokerOpts.WebsocketEnabled && !brokerOpts.TCPEnabled { - return nil, errors.New("at least websocket or TCP must be enabled") - } - - broker := mqtt.NewServer(&mqtt.Options{ - BufferSize: brokerOpts.BufferSize, - BufferBlockSize: brokerOpts.BufferBlockSize, - InflightTTL: 30, - }) - - if brokerOpts.WebsocketEnabled { - // check websocket bind address - _, _, err := net.SplitHostPort(brokerOpts.WebsocketBindAddress) - if err != nil { - return nil, fmt.Errorf("parsing websocket bind address (%s) failed: %w", brokerOpts.WebsocketBindAddress, err) - } - - ws := listeners.NewWebsocket("ws1", brokerOpts.WebsocketBindAddress) - if err := broker.AddListener(ws, &listeners.Config{ - Auth: &AuthAllowEveryone{}, - TLS: nil, - }); err != nil { - return nil, fmt.Errorf("adding websocket listener failed: %w", err) - } - } - - if brokerOpts.TCPEnabled { - // check tcp bind address - _, _, err := net.SplitHostPort(brokerOpts.TCPBindAddress) - if err != nil { - return nil, fmt.Errorf("parsing TCP bind address (%s) failed: %w", brokerOpts.TCPBindAddress, err) - } - - tcp := listeners.NewTCP("t1", brokerOpts.TCPBindAddress) - - var tcpAuthController auth.Controller - if brokerOpts.TCPAuthEnabled { - var err error - tcpAuthController, err = NewAuthAllowUsers(brokerOpts.TCPAuthPasswordSalt, brokerOpts.TCPAuthUsers) - if err != nil { - return nil, fmt.Errorf("enabling TCP Authentication failed: %w", err) - } - } else { - tcpAuthController = &AuthAllowEveryone{} - } - - var tlsConfig *tls.Config - if brokerOpts.TCPTLSEnabled { - var err error - - tlsConfig, err = NewTLSConfig(brokerOpts.TCPTLSCertificatePath, brokerOpts.TCPTLSPrivateKeyPath) - if err != nil { - return nil, fmt.Errorf("enabling TCP TLS failed: %w", err) - } - } - - if err := broker.AddListener(tcp, &listeners.Config{ - Auth: tcpAuthController, - TLSConfig: tlsConfig, - }); err != nil { - return nil, fmt.Errorf("adding TCP listener failed: %w", err) - } - } - - s := subscriptionmanager.New( - subscriptionmanager.WithMaxTopicSubscriptionsPerClient[string, string](brokerOpts.MaxTopicSubscriptionsPerClient), - subscriptionmanager.WithCleanupThresholdCount[string, string](brokerOpts.TopicCleanupThresholdCount), - subscriptionmanager.WithCleanupThresholdRatio[string, string](brokerOpts.TopicCleanupThresholdRatio), - ) - - // this event is used to drop malicious clients - unhook := s.Events().DropClient.Hook(func(event *subscriptionmanager.DropClientEvent[string]) { - client, exists := broker.Clients.Get(event.ClientID) - if !exists { - return - } - - // stop the client connection - client.Stop(event.Reason) - - // delete the client from the broker - broker.Clients.Delete(event.ClientID) - }).Unhook - - // bind the broker events to the SubscriptionManager to track the subscriptions - broker.Events.OnConnect = func(cl events.Client, pk events.Packet) { - s.Connect(cl.ID) - } - - broker.Events.OnDisconnect = func(cl events.Client, err error) { - s.Disconnect(cl.ID) - } - - broker.Events.OnSubscribe = func(topic string, cl events.Client, qos byte) { - s.Subscribe(cl.ID, topic) - } - - broker.Events.OnUnsubscribe = func(topic string, cl events.Client) { - s.Unsubscribe(cl.ID, topic) - } - - return &Broker{ - broker: broker, - opts: brokerOpts, - subscriptionManager: s, - unhook: unhook, - }, nil -} - -func (b *Broker) Events() subscriptionmanager.Events[string, string] { - return *b.subscriptionManager.Events() -} - -// Start the broker. -func (b *Broker) Start() error { - return b.broker.Serve() -} - -// Stop the broker. -func (b *Broker) Stop() error { - if b.unhook != nil { - b.unhook() - } - - return b.broker.Close() -} - -// SystemInfo returns the metrics of the broker. -func (b *Broker) SystemInfo() *system.Info { - return b.broker.System -} - -func (b *Broker) HasSubscribers(topic string) bool { - return b.subscriptionManager.TopicHasSubscribers(topic) -} - -// Send publishes a message. -func (b *Broker) Send(topic string, payload []byte) error { - return b.broker.Publish(topic, payload, false) -} - -// SubscribersSize returns the size of the underlying map of the SubscriptionManager. -func (b *Broker) SubscribersSize() int { - return b.subscriptionManager.SubscribersSize() -} - -// TopicsSize returns the size of all underlying maps of the SubscriptionManager. -func (b *Broker) TopicsSize() int { - return b.subscriptionManager.TopicsSize() -} diff --git a/pkg/mqtt/options.go b/pkg/mqtt/options.go new file mode 100644 index 0000000..45c1c36 --- /dev/null +++ b/pkg/mqtt/options.go @@ -0,0 +1,54 @@ +package mqtt + +// Options are options around the MQTT Server. +type Options struct { + // WebsocketEnabled defines whether to enable the websocket connection of the MQTT broker. + WebsocketEnabled bool + // WebsocketBindAddress defines the websocket bind address on which the MQTT broker listens on. + WebsocketBindAddress string + // WebsocketAdvertiseAddress defines the address of the websocket of the MQTT broker which is advertised to the INX Server (optional). + WebsocketAdvertiseAddress string +} + +var defaultOpts = []Option{ + WithWebsocketEnabled(true), + WithWebsocketBindAddress("localhost:1888"), + WithWebsocketAdvertiseAddress(""), +} + +// applies the given Option. +func (so *Options) apply(opts ...Option) { + for _, opt := range opts { + opt(so) + } +} + +// ApplyOnDefault applies the given options on top of the default options. +func (so *Options) ApplyOnDefault(opts ...Option) { + so.apply(defaultOpts...) + so.apply(opts...) +} + +// Option is a function which sets an option on a Options instance. +type Option func(options *Options) + +// WithWebsocketEnabled sets whether to enable the websocket connection of the MQTT broker. +func WithWebsocketEnabled(websocketEnabled bool) Option { + return func(options *Options) { + options.WebsocketEnabled = websocketEnabled + } +} + +// WithWebsocketBindAddress sets the websocket bind address on which the MQTT broker listens on. +func WithWebsocketBindAddress(websocketBindAddress string) Option { + return func(options *Options) { + options.WebsocketBindAddress = websocketBindAddress + } +} + +// WithWebsocketBindAddress sets the address of the websocket of the MQTT broker which is advertised to the INX Server (optional). +func WithWebsocketAdvertiseAddress(websocketAdvertiseAddress string) Option { + return func(options *Options) { + options.WebsocketAdvertiseAddress = websocketAdvertiseAddress + } +} diff --git a/pkg/mqtt/publish.go b/pkg/mqtt/publish.go new file mode 100644 index 0000000..d55624d --- /dev/null +++ b/pkg/mqtt/publish.go @@ -0,0 +1,208 @@ +package mqtt + +import ( + "context" + + "github.com/iotaledger/inx-app/pkg/nodebridge" + iotago "github.com/iotaledger/iota.go/v4" + iotaapi "github.com/iotaledger/iota.go/v4/api" +) + +func (s *Server) sendMessageOnTopic(topic string, payload []byte) { + if err := s.MQTTBroker.Send(topic, payload); err != nil { + s.LogWarnf("failed to send message on topic %s, error: %s", topic, err.Error()) + } +} + +func (s *Server) publishWithPayloadFuncOnTopicsIfSubscribed(payloadFunc func() ([]byte, error), topics ...string) error { + var payload []byte + + for _, topic := range topics { + if !s.MQTTBroker.HasSubscribers(topic) { + continue + } + + if payload == nil { + // payload is not yet set, so we need to call the payloadFunc + var err error + payload, err = payloadFunc() + if err != nil { + return err + } + } + + s.sendMessageOnTopic(topic, payload) + } + + return nil +} + +func addTopicsPrefix(topics []string, prefix string) []string { + rawTopics := make([]string, len(topics)) + + for i, topic := range topics { + rawTopics[i] = topic + prefix + } + + return rawTopics +} + +func (s *Server) publishWithPayloadFuncsOnTopicsIfSubscribed(jsonPayloadFunc func() ([]byte, error), rawPayloadFunc func() ([]byte, error), topics ...string) error { + // publish JSON payload + if err := s.publishWithPayloadFuncOnTopicsIfSubscribed(func() ([]byte, error) { + return jsonPayloadFunc() + }, topics...); err != nil { + return err + } + + // publish raw payload + if err := s.publishWithPayloadFuncOnTopicsIfSubscribed(func() ([]byte, error) { + return rawPayloadFunc() + }, addTopicsPrefix(topics, "/raw")...); err != nil { + return err + } + + return nil +} + +func (s *Server) publishPayloadOnTopicsIfSubscribed(apiFunc func() (iotago.API, error), payloadFunc func() (any, error), topics ...string) error { + return s.publishWithPayloadFuncsOnTopicsIfSubscribed(func() ([]byte, error) { + api, err := apiFunc() + if err != nil { + return nil, err + } + + payload, err := payloadFunc() + if err != nil { + return nil, err + } + + return api.JSONEncode(payload) + }, func() ([]byte, error) { + api, err := apiFunc() + if err != nil { + return nil, err + } + + payload, err := payloadFunc() + if err != nil { + return nil, err + } + + return api.Encode(payload) + }, topics...) +} + +func (s *Server) publishCommitmentOnTopicIfSubscribed(topic string, commitmentFunc func() (*iotago.Commitment, error)) error { + return s.publishPayloadOnTopicsIfSubscribed( + func() (iotago.API, error) { + commitment, err := commitmentFunc() + if err != nil { + return nil, err + } + + return s.NodeBridge.APIProvider().APIForVersion(commitment.ProtocolVersion) + }, + func() (any, error) { + return commitmentFunc() + }, + topic, + ) +} + +func (s *Server) blockTopicsForBasicBlock(basicBlockBody *iotago.BasicBlockBody) []string { + blockTopics := []string{TopicBlocksBasic} + + switch payload := basicBlockBody.Payload.(type) { + case *iotago.SignedTransaction: + blockTopics = append(blockTopics, TopicBlocksBasicTransaction) + + //nolint:gocritic // the type switch is nicer here + switch p := payload.Transaction.Payload.(type) { + case *iotago.TaggedData: + blockTopics = append(blockTopics, TopicBlocksBasicTransactionTaggedData) + if len(p.Tag) > 0 { + blockTopics = append(blockTopics, GetTopicBlocksBasicTransactionTaggedDataTag(p.Tag)) + } + } + + case *iotago.TaggedData: + blockTopics = append(blockTopics, TopicBlocksBasicTaggedData) + if len(payload.Tag) > 0 { + blockTopics = append(blockTopics, GetTopicBlocksBasicTaggedDataTag(payload.Tag)) + } + } + + return blockTopics +} + +func (s *Server) publishBlockIfSubscribed(block *iotago.Block, rawData []byte) error { + // always publish every block on the "blocks" topic + blockTopics := []string{TopicBlocks} + + switch blockBody := block.Body.(type) { + case *iotago.BasicBlockBody: + blockTopics = append(blockTopics, s.blockTopicsForBasicBlock(blockBody)...) + case *iotago.ValidationBlockBody: + blockTopics = append(blockTopics, TopicBlocksValidation) + default: + s.LogWarnf("unknown block body type: %T", blockBody) + } + + return s.publishWithPayloadFuncsOnTopicsIfSubscribed(func() ([]byte, error) { + return block.API.JSONEncode(block) + }, func() ([]byte, error) { + return rawData, nil + }, blockTopics...) +} + +func (s *Server) publishBlockMetadataOnTopicsIfSubscribed(metadataFunc func() (*iotaapi.BlockMetadataResponse, error), topics ...string) error { + return s.publishPayloadOnTopicsIfSubscribed( + func() (iotago.API, error) { return s.NodeBridge.APIProvider().CommittedAPI(), nil }, + func() (any, error) { + return metadataFunc() + }, + topics..., + ) +} + +func (s *Server) publishOutputIfSubscribed(ctx context.Context, output *nodebridge.Output, publishOnAllTopics bool) error { + topics := []string{GetTopicOutput(output.OutputID)} + + if publishOnAllTopics { + // If this is the first output in a transaction (index 0), then check if someone is observing the transaction that generated this output + if output.Metadata.Spent == nil && output.OutputID.Index() == 0 { + s.fetchAndPublishTransactionInclusionWithBlock(ctx, + output.OutputID.TransactionID(), + func() (iotago.BlockID, error) { + return output.Metadata.BlockID, nil + }, + ) + } + + bech32HRP := s.NodeBridge.APIProvider().CommittedAPI().ProtocolParameters().Bech32HRP() + topics = append(topics, GetChainTopicsForOutput(output.OutputID, output.Output, bech32HRP)...) + topics = append(topics, GetUnlockConditionTopicsForOutput(TopicOutputsByUnlockConditionAndAddress, output.Output, bech32HRP)...) + } + + var payload *iotaapi.OutputWithMetadataResponse + payloadFuncCached := func() (any, error) { + if payload == nil { + payload = &iotaapi.OutputWithMetadataResponse{ + Output: output.Output, + OutputIDProof: output.OutputIDProof, + Metadata: output.Metadata, + } + } + + return payload, nil + } + + return s.publishPayloadOnTopicsIfSubscribed( + func() (iotago.API, error) { + return s.NodeBridge.APIProvider().CommittedAPI(), nil + }, + payloadFuncCached, + topics..., + ) +} diff --git a/pkg/mqtt/server.go b/pkg/mqtt/server.go new file mode 100644 index 0000000..cd385cc --- /dev/null +++ b/pkg/mqtt/server.go @@ -0,0 +1,591 @@ +package mqtt + +import ( + "context" + "math/rand" + "strings" + "sync" + "time" + + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/iotaledger/hive.go/app/shutdown" + "github.com/iotaledger/hive.go/ierrors" + "github.com/iotaledger/hive.go/lo" + "github.com/iotaledger/hive.go/logger" + "github.com/iotaledger/hive.go/web/subscriptionmanager" + "github.com/iotaledger/inx-app/pkg/nodebridge" + "github.com/iotaledger/inx-mqtt/pkg/broker" + iotago "github.com/iotaledger/iota.go/v4" + iotaapi "github.com/iotaledger/iota.go/v4/api" +) + +const ( + APIRoute = "mqtt/v2" +) + +const ( + GrpcListenToBlocks = "INX.ListenToBlocks" + GrpcListenToAcceptedBlocks = "INX.ListenToAcceptedBlocks" + GrpcListenToConfirmedBlocks = "INX.ListenToConfirmedBlocks" + GrpcListenToAcceptedTransactions = "INX.ListenToAcceptedTransactions" + GrpcListenToLedgerUpdates = "INX.ListenToLedgerUpdates" +) + +const ( + fetchTimeout = 5 * time.Second +) + +type grpcSubcription struct { + //nolint:containedctx + context context.Context + cancelFunc func() + count int + identifier int +} + +type Server struct { + *logger.WrappedLogger + + MQTTBroker broker.Broker + NodeBridge nodebridge.NodeBridge + shutdownHandler *shutdown.ShutdownHandler + serverOptions *Options + + grpcSubscriptionsLock sync.Mutex + grpcSubscriptions map[string]*grpcSubcription + + cleanupFunc func() +} + +func NewServer(log *logger.Logger, + bridge nodebridge.NodeBridge, + broker broker.Broker, + shutdownHandler *shutdown.ShutdownHandler, + serverOpts ...Option) (*Server, error) { + opts := &Options{} + opts.ApplyOnDefault(serverOpts...) + + s := &Server{ + WrappedLogger: logger.NewWrappedLogger(log), + NodeBridge: bridge, + shutdownHandler: shutdownHandler, + MQTTBroker: broker, + serverOptions: opts, + grpcSubscriptions: make(map[string]*grpcSubcription), + } + + return s, nil +} + +func (s *Server) Start(ctx context.Context) error { + + // register broker events + unhookBrokerEvents := lo.Batch( + s.MQTTBroker.Events().ClientConnected.Hook(func(event *subscriptionmanager.ClientEvent[string]) { + s.onClientConnect(event.ClientID) + }).Unhook, + s.MQTTBroker.Events().ClientDisconnected.Hook(func(event *subscriptionmanager.ClientEvent[string]) { + s.onClientDisconnect(event.ClientID) + }).Unhook, + s.MQTTBroker.Events().TopicSubscribed.Hook(func(event *subscriptionmanager.ClientTopicEvent[string, string]) { + s.onSubscribeTopic(ctx, event.ClientID, event.Topic) + }).Unhook, + s.MQTTBroker.Events().TopicUnsubscribed.Hook(func(event *subscriptionmanager.ClientTopicEvent[string, string]) { + s.onUnsubscribeTopic(event.ClientID, event.Topic) + }).Unhook, + ) + + if err := s.MQTTBroker.Start(); err != nil { + return ierrors.Wrap(err, "failed to start MQTT broker") + } + + if s.serverOptions.WebsocketEnabled { + ctxRegister, cancelRegister := context.WithTimeout(ctx, 5*time.Second) + + s.LogInfo("Registering API route ...") + + advertisedAddress := s.serverOptions.WebsocketBindAddress + if s.serverOptions.WebsocketAdvertiseAddress != "" { + advertisedAddress = s.serverOptions.WebsocketAdvertiseAddress + } + + if err := s.NodeBridge.RegisterAPIRoute(ctxRegister, APIRoute, advertisedAddress, ""); err != nil { + s.LogErrorfAndExit("failed to register API route via INX: %s", err.Error()) + } + s.LogInfo("Registering API route ... done") + cancelRegister() + } + + // register node bridge events + unhookNodeBridgeEvents := lo.Batch( + s.NodeBridge.Events().LatestCommitmentChanged.Hook(func(c *nodebridge.Commitment) { + if err := s.publishCommitmentOnTopicIfSubscribed(TopicCommitmentsLatest, func() (*iotago.Commitment, error) { return c.Commitment, nil }); err != nil { + s.LogWarnf("failed to publish latest commitment: %s", err.Error()) + } + }).Unhook, + + s.NodeBridge.Events().LatestFinalizedCommitmentChanged.Hook(func(c *nodebridge.Commitment) { + if err := s.publishCommitmentOnTopicIfSubscribed(TopicCommitmentsFinalized, func() (*iotago.Commitment, error) { return c.Commitment, nil }); err != nil { + s.LogWarnf("failed to publish latest finalized commitment: %s", err.Error()) + } + }).Unhook, + ) + + s.cleanupFunc = lo.Batch( + unhookBrokerEvents, + unhookNodeBridgeEvents, + ) + + s.LogInfo("Starting MQTT Broker ... done") + + return nil +} + +func (s *Server) Stop() error { + s.LogInfo("Stopping MQTT Broker ...") + + if s.cleanupFunc != nil { + s.cleanupFunc() + } + + if s.serverOptions.WebsocketEnabled { + ctxUnregister, cancelUnregister := context.WithTimeout(context.Background(), 5*time.Second) + + s.LogInfo("Removing API route ...") + //nolint:contextcheck // false positive + if err := s.NodeBridge.UnregisterAPIRoute(ctxUnregister, APIRoute); err != nil { + s.LogErrorf("failed to remove API route via INX: %s", err.Error()) + } + cancelUnregister() + } + + if err := s.MQTTBroker.Stop(); err != nil { + return ierrors.Wrap(err, "failed to stop MQTT broker") + } + + s.LogInfo("Stopping MQTT Broker ...done") + + return nil +} + +func (s *Server) Run(ctx context.Context) { + if err := s.Start(ctx); err != nil { + s.LogErrorAndExit(err.Error()) + } + + <-ctx.Done() + + //nolint:contextcheck // false positive + if err := s.Stop(); err != nil { + s.LogError(err.Error()) + } +} + +func (s *Server) onClientConnect(clientID string) { + s.LogDebugf("onClientConnect %s", clientID) +} + +func (s *Server) onClientDisconnect(clientID string) { + s.LogDebugf("onClientDisconnect %s", clientID) +} + +func (s *Server) onSubscribeTopic(ctx context.Context, clientID string, topic string) { + s.LogDebugf("client %s subscribed to %s", clientID, topic) + + switch topic { + case TopicCommitmentsLatest: + // we don't need to subscribe here, because this is handled by the node bridge events + // but we need to publish the latest payload once to the new subscriber + go s.fetchAndPublishLatestCommitmentTopic() + + case TopicCommitmentsFinalized: + // we don't need to subscribe here, because this is handled by the node bridge events + // but we need to publish the latest payload once to the new subscriber + go s.fetchAndPublishFinalizedCommitmentTopic() + + case TopicBlocks, + TopicBlocksValidation, + TopicBlocksBasic, + TopicBlocksBasicTransaction, + TopicBlocksBasicTransactionTaggedData, + TopicBlocksBasicTaggedData: + s.startListenIfNeeded(ctx, GrpcListenToBlocks, s.listenToBlocks) + + case TopicBlockMetadataAccepted: + s.startListenIfNeeded(ctx, GrpcListenToAcceptedBlocks, s.listenToAcceptedBlocksMetadata) + + case TopicBlockMetadataConfirmed: + s.startListenIfNeeded(ctx, GrpcListenToConfirmedBlocks, s.listenToConfirmedBlocksMetadata) + + default: + switch { + case strings.HasPrefix(topic, "blocks/basic/") && strings.Contains(topic, "tagged-data/"): + // topicBlocksBasicTransactionTaggedDataTag + // topicBlocksBasicTaggedDataTag + s.startListenIfNeeded(ctx, GrpcListenToBlocks, s.listenToBlocks) + + case strings.HasPrefix(topic, "block-metadata/"): + // topicBlockMetadata + // HINT: it can't be topicBlockMetadataAccepted or topicBlockMetadataConfirmed because they are handled above + // so it must be a blockID + if blockID := BlockIDFromBlockMetadataTopic(topic); !blockID.Empty() { + // start listening to accepted and confirmed blocks if not already done to get state updates for that blockID + s.startListenIfNeeded(ctx, GrpcListenToAcceptedBlocks, s.listenToAcceptedBlocksMetadata) + s.startListenIfNeeded(ctx, GrpcListenToConfirmedBlocks, s.listenToConfirmedBlocksMetadata) + + go s.fetchAndPublishBlockMetadata(ctx, blockID) + } + + case strings.HasPrefix(topic, "outputs/") || strings.HasPrefix(topic, "transactions/"): + // topicOutputs + // topicAccountOutputs + // topicAnchorOutputs + // topicFoundryOutputs + // topicNFTOutputs + // topicOutputsByUnlockConditionAndAddress + // topicSpentOutputsByUnlockConditionAndAddress + // topicTransactionsIncludedBlock + s.startListenIfNeeded(ctx, GrpcListenToAcceptedTransactions, s.listenToAcceptedTransactions) + s.startListenIfNeeded(ctx, GrpcListenToLedgerUpdates, s.listenToLedgerUpdates) + + if transactionID := TransactionIDFromTransactionsIncludedBlockTopic(topic); transactionID != iotago.EmptyTransactionID { + go s.fetchAndPublishTransactionInclusion(ctx, transactionID) + } + if outputID := OutputIDFromOutputsTopic(topic); outputID != iotago.EmptyOutputID { + go s.fetchAndPublishOutput(ctx, outputID) + } + } + } +} + +func (s *Server) onUnsubscribeTopic(clientID string, topic string) { + s.LogDebugf("client %s unsubscribed from %s", clientID, topic) + + switch topic { + + case TopicCommitmentsLatest, + TopicCommitmentsFinalized: + // we don't need to unsubscribe here, because this is handled by the node bridge events anyway. + + case TopicBlocks, + TopicBlocksValidation, + TopicBlocksBasic, + TopicBlocksBasicTransaction, + TopicBlocksBasicTransactionTaggedData, + TopicBlocksBasicTaggedData: + s.stopListenIfNeeded(GrpcListenToBlocks) + + case TopicBlockMetadataAccepted: + s.stopListenIfNeeded(GrpcListenToAcceptedBlocks) + + case TopicBlockMetadataConfirmed: + s.stopListenIfNeeded(GrpcListenToConfirmedBlocks) + + default: + switch { + case strings.HasPrefix(topic, "blocks/basic/") && strings.Contains(topic, "tagged-data/"): + // topicBlocksBasicTransactionTaggedDataTag + // topicBlocksBasicTaggedDataTag + s.stopListenIfNeeded(GrpcListenToBlocks) + + case strings.HasPrefix(topic, "block-metadata/"): + // topicBlockMetadata + // it can't be topicBlockMetadataAccepted or topicBlockMetadataConfirmed because they are handled above + s.stopListenIfNeeded(GrpcListenToAcceptedBlocks) + s.stopListenIfNeeded(GrpcListenToConfirmedBlocks) + + case strings.HasPrefix(topic, "outputs/") || strings.HasPrefix(topic, "transactions/"): + // topicOutputs + // topicAccountOutputs + // topicAnchorOutputs + // topicFoundryOutputs + // topicNFTOutputs + // topicOutputsByUnlockConditionAndAddress + // topicSpentOutputsByUnlockConditionAndAddress + // topicTransactionsIncludedBlock + s.stopListenIfNeeded(GrpcListenToAcceptedTransactions) + s.stopListenIfNeeded(GrpcListenToLedgerUpdates) + } + } +} + +func (s *Server) addGRPCSubscription(ctx context.Context, grpcCall string) *grpcSubcription { + s.grpcSubscriptionsLock.Lock() + defer s.grpcSubscriptionsLock.Unlock() + + if sub, ok := s.grpcSubscriptions[grpcCall]; ok { + // subscription already exists + // => increase count to track subscribers + sub.count++ + + return nil + } + + ctxCancel, cancel := context.WithCancel(ctx) + + sub := &grpcSubcription{ + count: 1, + context: ctxCancel, + cancelFunc: cancel, + identifier: rand.Int(), //nolint:gosec // we do not care about weak random numbers here + } + s.grpcSubscriptions[grpcCall] = sub + + return sub +} + +func (s *Server) removeGRPCSubscription(grpcCall string, subscriptionIdentifier int) { + s.grpcSubscriptionsLock.Lock() + defer s.grpcSubscriptionsLock.Unlock() + + if sub, ok := s.grpcSubscriptions[grpcCall]; ok && sub.identifier == subscriptionIdentifier { + // Only delete if it was not already replaced by a new one. + delete(s.grpcSubscriptions, grpcCall) + } +} + +func (s *Server) startListenIfNeeded(ctx context.Context, grpcCall string, listenFunc func(context.Context) error) { + sub := s.addGRPCSubscription(ctx, grpcCall) + if sub == nil { + // subscription already exists + return + } + + go func() { + s.LogInfof("Listen to %s", grpcCall) + + if err := listenFunc(sub.context); err != nil && !errors.Is(err, context.Canceled) { + s.LogErrorf("Finished listen to %s with error: %s", grpcCall, err.Error()) + if status.Code(err) == codes.Unavailable && s.shutdownHandler != nil { + s.shutdownHandler.SelfShutdown("INX became unavailable", true) + } + } else { + s.LogInfof("Finished listen to %s", grpcCall) + } + + s.removeGRPCSubscription(grpcCall, sub.identifier) + }() +} + +func (s *Server) stopListenIfNeeded(grpcCall string) { + s.grpcSubscriptionsLock.Lock() + defer s.grpcSubscriptionsLock.Unlock() + + sub, ok := s.grpcSubscriptions[grpcCall] + if ok { + // subscription found + // decrease amount of subscribers + sub.count-- + + if sub.count == 0 { + // => no more subscribers => stop listening + sub.cancelFunc() + delete(s.grpcSubscriptions, grpcCall) + } + } +} + +func (s *Server) listenToBlocks(ctx context.Context) error { + return s.NodeBridge.ListenToBlocks(ctx, func(block *iotago.Block, rawData []byte) error { + if err := s.publishBlockIfSubscribed(block, rawData); err != nil { + s.LogErrorf("failed to publish block: %v", err) + } + + // we don't return an error here, because we want to continue listening even if publishing fails once + return nil + }) +} + +func (s *Server) listenToAcceptedBlocksMetadata(ctx context.Context) error { + return s.NodeBridge.ListenToAcceptedBlocks(ctx, func(blockMetadata *iotaapi.BlockMetadataResponse) error { + if err := s.publishBlockMetadataOnTopicsIfSubscribed(func() (*iotaapi.BlockMetadataResponse, error) { return blockMetadata, nil }, + TopicBlockMetadataAccepted, + GetTopicBlockMetadata(blockMetadata.BlockID), + ); err != nil { + s.LogErrorf("failed to publish accepted block metadata: %v", err) + } + + // we don't return an error here, because we want to continue listening even if publishing fails once + return nil + }) +} + +func (s *Server) listenToConfirmedBlocksMetadata(ctx context.Context) error { + return s.NodeBridge.ListenToConfirmedBlocks(ctx, func(blockMetadata *iotaapi.BlockMetadataResponse) error { + if err := s.publishBlockMetadataOnTopicsIfSubscribed(func() (*iotaapi.BlockMetadataResponse, error) { return blockMetadata, nil }, + TopicBlockMetadataConfirmed, + GetTopicBlockMetadata(blockMetadata.BlockID), + ); err != nil { + s.LogErrorf("failed to publish confirmed block metadata: %v", err) + } + + // we don't return an error here, because we want to continue listening even if publishing fails once + return nil + }) +} + +func (s *Server) listenToAcceptedTransactions(ctx context.Context) error { + return s.NodeBridge.ListenToAcceptedTransactions(ctx, func(payload *nodebridge.AcceptedTransaction) error { + for _, consumed := range payload.Consumed { + if err := s.publishOutputIfSubscribed(ctx, consumed, true); err != nil { + s.LogErrorf("failed to publish spent output in listen to accepted transaction update: %v", err) + } + } + + for _, created := range payload.Created { + if err := s.publishOutputIfSubscribed(ctx, created, true); err != nil { + s.LogErrorf("failed to publish created output in listen to accepted transaction update: %v", err) + } + } + + // we don't return an error here, because we want to continue listening even if publishing fails once + return nil + }) +} + +func (s *Server) listenToLedgerUpdates(ctx context.Context) error { + return s.NodeBridge.ListenToLedgerUpdates(ctx, 0, 0, func(payload *nodebridge.LedgerUpdate) error { + for _, consumed := range payload.Consumed { + if err := s.publishOutputIfSubscribed(ctx, consumed, true); err != nil { + s.LogErrorf("failed to publish spent output in ledger update: %v", err) + } + } + + for _, created := range payload.Created { + if err := s.publishOutputIfSubscribed(ctx, created, true); err != nil { + s.LogErrorf("failed to publish created output in ledger update: %v", err) + } + } + + // we don't return an error here, because we want to continue listening even if publishing fails once + return nil + }) +} + +func (s *Server) fetchAndPublishLatestCommitmentTopic() { + if err := s.publishCommitmentOnTopicIfSubscribed(TopicCommitmentsLatest, + func() (*iotago.Commitment, error) { + latestCommitment := s.NodeBridge.LatestCommitment() + if latestCommitment == nil { + return nil, ierrors.New("failed to retrieve latest commitment") + } + + return latestCommitment.Commitment, nil + }, + ); err != nil { + s.LogErrorf("failed to publish latest commitment: %v", err) + } +} + +func (s *Server) fetchAndPublishFinalizedCommitmentTopic() { + if err := s.publishCommitmentOnTopicIfSubscribed(TopicCommitmentsFinalized, + func() (*iotago.Commitment, error) { + latestFinalizedCommitment := s.NodeBridge.LatestFinalizedCommitment() + if latestFinalizedCommitment == nil { + return nil, ierrors.New("failed to retrieve latest finalized commitment") + } + + return latestFinalizedCommitment.Commitment, nil + }, + ); err != nil { + s.LogErrorf("failed to publish latest finalized commitment: %v", err) + } +} + +func (s *Server) fetchAndPublishBlockMetadata(ctx context.Context, blockID iotago.BlockID) { + if err := s.publishBlockMetadataOnTopicsIfSubscribed(func() (*iotaapi.BlockMetadataResponse, error) { + resp, err := s.NodeBridge.BlockMetadata(ctx, blockID) + if err != nil { + return nil, ierrors.Wrapf(err, "failed to retrieve block metadata %s", blockID.ToHex()) + } + + return resp, nil + }, GetTopicBlockMetadata(blockID)); err != nil { + s.LogErrorf("failed to publish block metadata %s: %v", blockID.ToHex(), err) + } +} + +func (s *Server) fetchAndPublishOutput(ctx context.Context, outputID iotago.OutputID) { + // we need to fetch the output to figure out which topics we need to publish on + output, err := s.NodeBridge.Output(ctx, outputID) + if err != nil { + s.LogErrorf("failed to retrieve output %s: %v", outputID.ToHex(), err) + return + } + + if err := s.publishOutputIfSubscribed(ctx, output, false); err != nil { + s.LogErrorf("failed to publish output %s: %v", outputID.ToHex(), err) + } +} + +func (s *Server) fetchAndPublishTransactionInclusion(ctx context.Context, transactionID iotago.TransactionID) { + + var blockID iotago.BlockID + blockIDFunc := func() (iotago.BlockID, error) { + if blockID.Empty() { + // get the output and then the blockID of the transaction that created the output + outputID := iotago.OutputID{} + copy(outputID[:], transactionID[:]) + + ctxFetch, cancelFetch := context.WithTimeout(ctx, fetchTimeout) + defer cancelFetch() + + output, err := s.NodeBridge.Output(ctxFetch, outputID) + if err != nil { + return iotago.EmptyBlockID, ierrors.Wrapf(err, "failed to retrieve output of transaction %s", transactionID.ToHex()) + } + + return output.Metadata.BlockID, nil + } + + return blockID, nil + } + + s.fetchAndPublishTransactionInclusionWithBlock(ctx, transactionID, blockIDFunc) +} + +func (s *Server) fetchAndPublishTransactionInclusionWithBlock(ctx context.Context, transactionID iotago.TransactionID, blockIDFunc func() (iotago.BlockID, error)) { + ctxFetch, cancelFetch := context.WithTimeout(ctx, fetchTimeout) + defer cancelFetch() + + var block *iotago.Block + blockFunc := func() (*iotago.Block, error) { + if block != nil { + return block, nil + } + + blockID, err := blockIDFunc() + if err != nil { + return nil, err + } + + resp, err := s.NodeBridge.Block(ctxFetch, blockID) + if err != nil { + s.LogErrorf("failed to retrieve block %s :%v", blockID.ToHex(), err) + return nil, err + } + block = resp + + return block, nil + } + + if err := s.publishPayloadOnTopicsIfSubscribed( + func() (iotago.API, error) { + block, err := blockFunc() + if err != nil { + return nil, err + } + + return block.API, nil + }, + func() (any, error) { + return blockFunc() + }, + GetTopicTransactionsIncludedBlock(transactionID), + ); err != nil { + s.LogErrorf("failed to publish transaction inclusion %s: %v", transactionID.ToHex(), err) + } +} diff --git a/pkg/mqtt/topics.go b/pkg/mqtt/topics.go new file mode 100644 index 0000000..228f1e1 --- /dev/null +++ b/pkg/mqtt/topics.go @@ -0,0 +1,275 @@ +//nolint:goconst +package mqtt + +import ( + "strings" + + iotago "github.com/iotaledger/iota.go/v4" + "github.com/iotaledger/iota.go/v4/hexutil" +) + +// Topic names. +const ( + ParameterBlockID = "{blockId}" + ParameterTransactionID = "{transactionId}" + ParameterOutputID = "{outputId}" + ParameterTag = "{tag}" + ParameterAccountAddress = "{accountAddress}" + ParameterAnchorAddress = "{anchorAddress}" + ParameterNFTAddress = "{nftAddress}" + ParameterFoundryID = "{foundryId}" + ParameterDelegationID = "{delegationId}" + ParameterAddress = "{address}" + ParameterCondition = "{condition}" + + topicSuffixAccepted = "accepted" + topicSuffixConfirmed = "confirmed" + + // HINT: all existing topics always have a "/raw" suffix for the raw payload as well. + TopicCommitmentsLatest = "commitments/latest" // iotago.Commitment + TopicCommitmentsFinalized = "commitments/finalized" // iotago.Commitment + + TopicBlocks = "blocks" // iotago.Block (track all incoming blocks) + TopicBlocksValidation = "blocks/validation" // iotago.Block (track all incoming validation blocks) + TopicBlocksBasic = "blocks/basic" // iotago.Block (track all incoming basic blocks) + TopicBlocksBasicTaggedData = "blocks/basic/tagged-data" // iotago.Block (track all incoming basic blocks with tagged data payload) + TopicBlocksBasicTaggedDataTag = "blocks/basic/tagged-data/" + ParameterTag // iotago.Block (track all incoming basic blocks with specific tagged data payload) + TopicBlocksBasicTransaction = "blocks/basic/transaction" // iotago.Block (track all incoming basic blocks with transactions) + TopicBlocksBasicTransactionTaggedData = "blocks/basic/transaction/tagged-data" // iotago.Block (track all incoming basic blocks with transactions and tagged data) + TopicBlocksBasicTransactionTaggedDataTag = "blocks/basic/transaction/tagged-data/" + ParameterTag // iotago.Block (track all incoming basic blocks with transactions and specific tagged data) + + // single block on subscribe and changes in it's metadata (accepted, confirmed). + TopicTransactionsIncludedBlock = "transactions/" + ParameterTransactionID + "/included-block" // api.BlockWithMetadataResponse (track inclusion of a single transaction) + + // single block on subscribe and changes in it's metadata (accepted, confirmed). + TopicBlockMetadata = "block-metadata/" + ParameterBlockID // api.BlockMetadataResponse (track changes to a single block) + + // all blocks that arrive after subscribing. + TopicBlockMetadataAccepted = "block-metadata/" + topicSuffixAccepted // api.BlockMetadataResponse (track acceptance of all blocks) + TopicBlockMetadataConfirmed = "block-metadata/" + topicSuffixConfirmed // api.BlockMetadataResponse (track confirmation of all blocks) + + // single output on subscribe and changes in it's metadata (accepted, committed, spent). + TopicOutputs = "outputs/" + ParameterOutputID // api.OutputWithMetadataResponse (track changes to a single output) + + // all outputs that arrive after subscribing (on transaction accepted and transaction committed). + TopicAccountOutputs = "outputs/account/" + ParameterAccountAddress // api.OutputWithMetadataResponse (all changes of the chain output) + TopicAnchorOutputs = "outputs/anchor/" + ParameterAnchorAddress // api.OutputWithMetadataResponse (all changes of the chain output) + TopicFoundryOutputs = "outputs/foundry/" + ParameterFoundryID // api.OutputWithMetadataResponse (all changes of the chain output) + TopicNFTOutputs = "outputs/nft/" + ParameterNFTAddress // api.OutputWithMetadataResponse (all changes of the chain output) + TopicDelegationOutputs = "outputs/delegation/" + ParameterDelegationID // api.OutputWithMetadataResponse (all changes of the chain output) + TopicOutputsByUnlockConditionAndAddress = "outputs/unlock/" + ParameterCondition + "/" + ParameterAddress // api.OutputWithMetadataResponse (all changes to outputs that match the unlock condition) +) + +type UnlockCondition string + +const ( + UnlockConditionAny UnlockCondition = "+" + UnlockConditionAddress UnlockCondition = "address" + UnlockConditionStorageReturn UnlockCondition = "storage-return" + UnlockConditionExpiration UnlockCondition = "expiration" + UnlockConditionStateController UnlockCondition = "state-controller" + UnlockConditionGovernor UnlockCondition = "governor" + UnlockConditionImmutableAccount UnlockCondition = "immutable-account" +) + +func BlockIDFromBlockMetadataTopic(topic string) iotago.BlockID { + if strings.HasPrefix(topic, "block-metadata/") && !(strings.HasSuffix(topic, topicSuffixAccepted) || strings.HasSuffix(topic, topicSuffixConfirmed)) { + blockIDHex := strings.Replace(topic, "block-metadata/", "", 1) + blockID, err := iotago.BlockIDFromHexString(blockIDHex) + if err != nil { + return iotago.EmptyBlockID + } + + return blockID + } + + return iotago.EmptyBlockID +} + +func TransactionIDFromTransactionsIncludedBlockTopic(topic string) iotago.TransactionID { + if strings.HasPrefix(topic, "transactions/") && strings.HasSuffix(topic, "/included-block") { + transactionIDHex := strings.Replace(topic, "transactions/", "", 1) + transactionIDHex = strings.Replace(transactionIDHex, "/included-block", "", 1) + + transactionID, err := iotago.TransactionIDFromHexString(transactionIDHex) + if err != nil || len(transactionID) != iotago.TransactionIDLength { + return iotago.EmptyTransactionID + } + + return transactionID + } + + return iotago.EmptyTransactionID +} + +func OutputIDFromOutputsTopic(topic string) iotago.OutputID { + if strings.HasPrefix(topic, "outputs/") && strings.Count(topic, "/") == 1 { + outputIDHex := strings.Replace(topic, "outputs/", "", 1) + outputID, err := iotago.OutputIDFromHexString(outputIDHex) + if err != nil { + return iotago.EmptyOutputID + } + + return outputID + } + + return iotago.EmptyOutputID +} + +func GetTopicBlocksBasicTaggedDataTag(tag []byte) string { + return strings.ReplaceAll(TopicBlocksBasicTaggedDataTag, ParameterTag, hexutil.EncodeHex(tag)) +} + +func GetTopicBlocksBasicTransactionTaggedDataTag(tag []byte) string { + return strings.ReplaceAll(TopicBlocksBasicTransactionTaggedDataTag, ParameterTag, hexutil.EncodeHex(tag)) +} + +func GetTopicBlockMetadata(blockID iotago.BlockID) string { + return strings.ReplaceAll(TopicBlockMetadata, ParameterBlockID, blockID.ToHex()) +} + +func GetTopicOutput(outputID iotago.OutputID) string { + return strings.ReplaceAll(TopicOutputs, ParameterOutputID, outputID.ToHex()) +} + +func GetTopicTransactionsIncludedBlock(transactionID iotago.TransactionID) string { + return strings.ReplaceAll(TopicTransactionsIncludedBlock, ParameterTransactionID, transactionID.ToHex()) +} + +func GetTopicAccountOutputs(accountID iotago.AccountID, hrp iotago.NetworkPrefix) string { + return strings.ReplaceAll(TopicAccountOutputs, ParameterAccountAddress, accountID.ToAddress().Bech32(hrp)) +} + +func GetTopicAnchorOutputs(anchorID iotago.AnchorID, hrp iotago.NetworkPrefix) string { + return strings.ReplaceAll(TopicAnchorOutputs, ParameterAnchorAddress, anchorID.ToAddress().Bech32(hrp)) +} + +func GetTopicFoundryOutputs(foundryID iotago.FoundryID) string { + return strings.ReplaceAll(TopicFoundryOutputs, ParameterFoundryID, foundryID.ToHex()) +} + +func GetTopicNFTOutputs(nftID iotago.NFTID, hrp iotago.NetworkPrefix) string { + return strings.ReplaceAll(TopicNFTOutputs, ParameterNFTAddress, nftID.ToAddress().Bech32(hrp)) +} + +func GetTopicDelegationOutputs(delegationID iotago.DelegationID) string { + return strings.ReplaceAll(TopicDelegationOutputs, ParameterDelegationID, delegationID.ToHex()) +} + +func GetTopicOutputsByUnlockConditionAndAddress(condition UnlockCondition, address iotago.Address, hrp iotago.NetworkPrefix) string { + topic := strings.ReplaceAll(TopicOutputsByUnlockConditionAndAddress, ParameterCondition, string(condition)) + return strings.ReplaceAll(topic, ParameterAddress, address.Bech32(hrp)) +} + +func GetUnlockConditionTopicsForOutput(baseTopic string, output iotago.Output, bech32HRP iotago.NetworkPrefix) []string { + topics := []string{} + + topicFunc := func(condition UnlockCondition, addressString string) string { + topic := strings.ReplaceAll(baseTopic, ParameterCondition, string(condition)) + return strings.ReplaceAll(topic, ParameterAddress, addressString) + } + + unlockConditions := output.UnlockConditionSet() + + // this tracks the addresses used by any unlock condition + // so that after checking all conditions we can see if anyone is subscribed to the wildcard + addressesToPublishForAny := make(map[string]struct{}) + + address := unlockConditions.Address() + if address != nil { + addr := address.Address.Bech32(bech32HRP) + topics = append(topics, topicFunc(UnlockConditionAddress, addr)) + addressesToPublishForAny[addr] = struct{}{} + } + + storageReturn := unlockConditions.StorageDepositReturn() + if storageReturn != nil { + addr := storageReturn.ReturnAddress.Bech32(bech32HRP) + topics = append(topics, topicFunc(UnlockConditionStorageReturn, addr)) + addressesToPublishForAny[addr] = struct{}{} + } + + expiration := unlockConditions.Expiration() + if expiration != nil { + addr := expiration.ReturnAddress.Bech32(bech32HRP) + topics = append(topics, topicFunc(UnlockConditionExpiration, addr)) + addressesToPublishForAny[addr] = struct{}{} + } + + stateController := unlockConditions.StateControllerAddress() + if stateController != nil { + addr := stateController.Address.Bech32(bech32HRP) + topics = append(topics, topicFunc(UnlockConditionStateController, addr)) + addressesToPublishForAny[addr] = struct{}{} + } + + governor := unlockConditions.GovernorAddress() + if governor != nil { + addr := governor.Address.Bech32(bech32HRP) + topics = append(topics, topicFunc(UnlockConditionGovernor, addr)) + addressesToPublishForAny[addr] = struct{}{} + } + + immutableAccount := unlockConditions.ImmutableAccount() + if immutableAccount != nil { + addr := immutableAccount.Address.Bech32(bech32HRP) + topics = append(topics, topicFunc(UnlockConditionImmutableAccount, addr)) + addressesToPublishForAny[addr] = struct{}{} + } + + for addr := range addressesToPublishForAny { + topics = append(topics, topicFunc(UnlockConditionAny, addr)) + } + + return topics +} + +func GetChainTopicsForOutput(outputID iotago.OutputID, output iotago.Output, bech32HRP iotago.NetworkPrefix) []string { + topics := []string{} + + switch o := output.(type) { + case *iotago.AccountOutput: + accountID := o.AccountID + if accountID.Empty() { + // Use implicit AccountID + accountID = iotago.AccountIDFromOutputID(outputID) + } + topics = append(topics, GetTopicAccountOutputs(accountID, bech32HRP)) + + case *iotago.AnchorOutput: + anchorID := o.AnchorID + if anchorID.Empty() { + // Use implicit AnchorID + anchorID = iotago.AnchorIDFromOutputID(outputID) + } + topics = append(topics, GetTopicAnchorOutputs(anchorID, bech32HRP)) + + case *iotago.FoundryOutput: + foundryID, err := o.FoundryID() + if err != nil { + return topics + } + topics = append(topics, GetTopicFoundryOutputs(foundryID)) + + case *iotago.NFTOutput: + nftID := o.NFTID + if nftID.Empty() { + // Use implicit NFTID + nftID = iotago.NFTIDFromOutputID(outputID) + } + topics = append(topics, GetTopicNFTOutputs(nftID, bech32HRP)) + + case *iotago.DelegationOutput: + delegationID := o.DelegationID + if delegationID.Empty() { + // Use implicit DelegationID + delegationID = iotago.DelegationIDFromOutputID(outputID) + } + topics = append(topics, GetTopicDelegationOutputs(delegationID)) + + default: + // BasicOutput + } + + return topics +} diff --git a/pkg/testsuite/broker_mock.go b/pkg/testsuite/broker_mock.go new file mode 100644 index 0000000..30f1018 --- /dev/null +++ b/pkg/testsuite/broker_mock.go @@ -0,0 +1,176 @@ +//nolint:revive // skip linter for this package name +package testsuite + +import ( + "sync" + "testing" + + "github.com/mochi-co/mqtt/server/system" + "github.com/stretchr/testify/require" + + "github.com/iotaledger/hive.go/web/subscriptionmanager" + "github.com/iotaledger/inx-mqtt/pkg/broker" +) + +type MockedBroker struct { + t *testing.T + + hasSubscribersCallback func(topic string) + subscriptionmanager *subscriptionmanager.SubscriptionManager[string, string] + + mockedSubscribedTopicsAndClientsLock sync.RWMutex + mockedSubscribedTopicsAndClients map[string]map[string]func(topic string, payload []byte) +} + +var _ broker.Broker = &MockedBroker{} + +func NewMockedBroker(t *testing.T) *MockedBroker { + t.Helper() + + broker := &MockedBroker{ + t: t, + hasSubscribersCallback: nil, + subscriptionmanager: subscriptionmanager.New[string, string](), + mockedSubscribedTopicsAndClients: make(map[string]map[string]func(topic string, payload []byte)), + } + + return broker +} + +// +// Broker interface +// + +func (m *MockedBroker) Events() *subscriptionmanager.Events[string, string] { + return m.subscriptionmanager.Events() +} + +func (m *MockedBroker) Start() error { + return nil +} + +func (m *MockedBroker) Stop() error { + return nil +} + +func (m *MockedBroker) HasSubscribers(topic string) bool { + // this callback is used in the testsuite to check if a message is + // about to be sent to a topic that was not expected to have subscribers + if m.hasSubscribersCallback != nil { + m.hasSubscribersCallback(topic) + } + + return m.subscriptionmanager.TopicHasSubscribers(topic) +} + +func (m *MockedBroker) Send(topic string, payload []byte) error { + m.mockedSubscribedTopicsAndClientsLock.RLock() + defer m.mockedSubscribedTopicsAndClientsLock.RUnlock() + + if _, ok := m.mockedSubscribedTopicsAndClients[topic]; ok { + // send to all subscribers + for _, callback := range m.mockedSubscribedTopicsAndClients[topic] { + if callback != nil { + callback(topic, payload) + } + } + } + + return nil +} + +func (m *MockedBroker) SystemInfo() *system.Info { + panic("not implemented") +} + +func (m *MockedBroker) SubscribersSize() int { + return m.subscriptionmanager.SubscribersSize() +} + +func (m *MockedBroker) TopicsSize() int { + return m.subscriptionmanager.TopicsSize() +} + +// +// Mock functions +// + +func (m *MockedBroker) MockClear() { + m.hasSubscribersCallback = nil + + // we can't replace the subscriptionmanager, otherwise the events will not be wired correctly + // so we need to manually disconnect all clients and remove all subscriptions + clientIDs := make(map[string]struct{}) + for _, clients := range m.mockedSubscribedTopicsAndClients { + for clientID := range clients { + clientIDs[clientID] = struct{}{} + } + } + + for clientID := range clientIDs { + m.MockClientDisconnected(clientID) + } + require.Equal(m.t, m.subscriptionmanager.TopicsSize(), 0, "topics not empty") + require.Equal(m.t, m.subscriptionmanager.SubscribersSize(), 0, "subscribers not empty") + + m.mockedSubscribedTopicsAndClients = make(map[string]map[string]func(topic string, payload []byte)) +} +func (m *MockedBroker) MockSetHasSubscribersCallback(hasSubscribersCallback func(topic string)) { + m.hasSubscribersCallback = hasSubscribersCallback +} + +func (m *MockedBroker) MockClientConnected(clientID string) { + m.subscriptionmanager.Connect(clientID) +} + +func (m *MockedBroker) MockClientDisconnected(clientID string) { + m.mockedSubscribedTopicsAndClientsLock.Lock() + defer m.mockedSubscribedTopicsAndClientsLock.Unlock() + + if !m.subscriptionmanager.Disconnect(clientID) { + require.FailNow(m.t, "client was not connected") + return + } + + // client was disconnected, so we need to remove all subscriptions + for topic, clients := range m.mockedSubscribedTopicsAndClients { + if _, exists := clients[clientID]; exists { + delete(clients, clientID) + if len(clients) == 0 { + delete(m.mockedSubscribedTopicsAndClients, topic) + } + } + } +} + +func (m *MockedBroker) MockTopicSubscribed(clientID string, topic string, callback func(topic string, payload []byte)) { + m.mockedSubscribedTopicsAndClientsLock.Lock() + defer m.mockedSubscribedTopicsAndClientsLock.Unlock() + + if !m.subscriptionmanager.Subscribe(clientID, topic) { + require.FailNow(m.t, "subscription failed") + return + } + + // topic was subscribed, so we need to add the callback + if _, ok := m.mockedSubscribedTopicsAndClients[topic]; !ok { + m.mockedSubscribedTopicsAndClients[topic] = make(map[string]func(topic string, payload []byte)) + } + m.mockedSubscribedTopicsAndClients[topic][clientID] = callback +} + +func (m *MockedBroker) MockTopicUnsubscribed(clientID string, topic string) { + m.mockedSubscribedTopicsAndClientsLock.Lock() + defer m.mockedSubscribedTopicsAndClientsLock.Unlock() + + if !m.subscriptionmanager.Unsubscribe(clientID, topic) { + require.FailNow(m.t, "unsubscription failed") + return + } + + // topic was unsubscribed, so we need to remove the callback + delete(m.mockedSubscribedTopicsAndClients[topic], clientID) + if len(m.mockedSubscribedTopicsAndClients[topic]) == 0 { + delete(m.mockedSubscribedTopicsAndClients, topic) + } +} diff --git a/pkg/testsuite/mqtt_test.go b/pkg/testsuite/mqtt_test.go new file mode 100644 index 0000000..77394c0 --- /dev/null +++ b/pkg/testsuite/mqtt_test.go @@ -0,0 +1,981 @@ +//nolint:forcetypeassert,scopelint,goconst,dupl +package testsuite_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/iotaledger/hive.go/lo" + "github.com/iotaledger/inx-app/pkg/nodebridge" + "github.com/iotaledger/inx-mqtt/pkg/mqtt" + "github.com/iotaledger/inx-mqtt/pkg/testsuite" + inx "github.com/iotaledger/inx/go" + iotago "github.com/iotaledger/iota.go/v4" + "github.com/iotaledger/iota.go/v4/api" + "github.com/iotaledger/iota.go/v4/tpkg" +) + +func TestMqttTopics(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ts.Run(ctx) + + type testTopic struct { + // the topic to subscribe to + topic string + // indicates that the message is received during the subscription to the topic. + // this is used to test the "polling mode". + isPollingTarget bool + // indicates that the message is received after the subscription to the topic. + // this is used to test the "event driven mode". + isEventTarget bool + } + + type test struct { + // the name of the test + name string + // the topics the test should subscribe to ("/raw" topics will be checked automatically) + topics []*testTopic + // the topics that should be ignored by the test (it's legit to receive messages on these topics) + topicsIgnore []string + // the expected JSON result received by the client on the subscribed topic + jsonTarget []byte + // the expected raw result received by the client on the subscribed topic + rawTarget []byte + // the function is called by the test before the MQTT topic is subscribed to (e.g. to inject test data) + preSubscribeFunc func() + // the function is called by the test after the MQTT topic is subscribed to (e.g. to inject test data) + postSubscribeFunc func() + } + + tests := []*test{ + + // ok - LatestCommitment + func() *test { + commitment := tpkg.RandCommitment() + + return &test{ + name: "ok - LatestCommitment", + topics: []*testTopic{ + { + topic: mqtt.TopicCommitmentsLatest, + // we receive the topic once during the subscription + // and a second time when the commitment is received + isPollingTarget: true, + isEventTarget: true, + }, + }, + topicsIgnore: []string{}, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(commitment)), + rawTarget: lo.PanicOnErr(ts.API().Encode(commitment)), + preSubscribeFunc: func() { + ts.SetLatestCommitment(&nodebridge.Commitment{ + CommitmentID: commitment.MustID(), + Commitment: commitment, + }) + }, + postSubscribeFunc: func() { + ts.ReceiveLatestCommitment(&nodebridge.Commitment{ + CommitmentID: commitment.MustID(), + Commitment: commitment, + }) + }, + } + }(), + + // ok - LatestFinalizedCommitment + func() *test { + commitment := tpkg.RandCommitment() + + return &test{ + name: "ok - LatestFinalizedCommitment", + topics: []*testTopic{ + { + topic: mqtt.TopicCommitmentsFinalized, + // we receive the topic once during the subscription + // and a second time when the commitment is received + isPollingTarget: true, + isEventTarget: true, + }, + }, + topicsIgnore: []string{}, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(commitment)), + rawTarget: lo.PanicOnErr(ts.API().Encode(commitment)), + preSubscribeFunc: func() { + ts.SetLatestFinalizedCommitment(&nodebridge.Commitment{ + CommitmentID: commitment.MustID(), + Commitment: commitment, + }) + }, + postSubscribeFunc: func() { + ts.ReceiveLatestFinalizedCommitment(&nodebridge.Commitment{ + CommitmentID: commitment.MustID(), + Commitment: commitment, + }) + }, + } + }(), + + // ok - Validation block + func() *test { + block := tpkg.RandBlock(tpkg.RandValidationBlockBody(ts.API()), ts.API(), iotago.Mana(500)) + + return &test{ + name: "ok - Validation block", + topics: []*testTopic{ + { + topic: mqtt.TopicBlocks, + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.TopicBlocksValidation, + isPollingTarget: false, + isEventTarget: true, + }, + }, + topicsIgnore: []string{}, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(block)), + rawTarget: lo.PanicOnErr(ts.API().Encode(block)), + postSubscribeFunc: func() { + ts.ReceiveBlock(&testsuite.MockedBlock{ + Block: block, + RawBlockData: lo.PanicOnErr(ts.API().Encode(block)), + }) + }, + } + }(), + + // ok - Basic block with tagged data + func() *test { + block := tpkg.RandBlock( + tpkg.RandBasicBlockBodyWithPayload(ts.API(), + &iotago.TaggedData{ + Tag: []byte("my tagged data payload"), + Data: []byte("some nice data"), + }, + ), ts.API(), iotago.Mana(500)) + + return &test{ + name: "ok - Basic block with tagged data", + topics: []*testTopic{ + { + topic: mqtt.TopicBlocks, + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.TopicBlocksBasic, + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.TopicBlocksBasicTaggedData, + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.GetTopicBlocksBasicTaggedDataTag([]byte("my tagged data payload")), + isPollingTarget: false, + isEventTarget: true, + }, + }, + topicsIgnore: []string{}, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(block)), + rawTarget: lo.PanicOnErr(ts.API().Encode(block)), + postSubscribeFunc: func() { + ts.ReceiveBlock(&testsuite.MockedBlock{ + Block: block, + RawBlockData: lo.PanicOnErr(ts.API().Encode(block)), + }) + }, + } + }(), + + // ok - Basic block with transaction and tagged data payload + func() *test { + testTx := ts.NewTestTransaction(true, tpkg.WithTxEssencePayload( + &iotago.TaggedData{ + Tag: []byte("my tagged data payload"), + Data: []byte("some nice data"), + }, + )) + + return &test{ + name: "ok - Basic block with transaction and tagged data payload", + topics: []*testTopic{ + { + topic: mqtt.TopicBlocks, + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.TopicBlocksBasic, + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.TopicBlocksBasicTransaction, + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.TopicBlocksBasicTransactionTaggedData, + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.GetTopicBlocksBasicTransactionTaggedDataTag([]byte("my tagged data payload")), + isPollingTarget: false, + isEventTarget: true, + }, + }, + topicsIgnore: []string{}, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(testTx.Block)), + rawTarget: lo.PanicOnErr(ts.API().Encode(testTx.Block)), + postSubscribeFunc: func() { + ts.ReceiveBlock(&testsuite.MockedBlock{ + Block: testTx.Block, + RawBlockData: lo.PanicOnErr(ts.API().Encode(testTx.Block)), + }) + }, + } + }(), + + // ok - Basic block with transaction and tagged data payload - TransactionsIncludedBlockTopic + func() *test { + testTx := ts.NewTestTransaction(true) + + return &test{ + name: "ok - Basic block with transaction and tagged data payload - TransactionsIncludedBlockTopic", + topics: []*testTopic{ + { + topic: mqtt.GetTopicTransactionsIncludedBlock(testTx.TransactionID), + isPollingTarget: true, + isEventTarget: true, + }, + }, + topicsIgnore: []string{ + mqtt.GetTopicOutput(testTx.ConsumedOutputID), + mqtt.GetTopicOutput(testTx.OutputID), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAny, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAddress, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + }, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(testTx.Block)), + rawTarget: lo.PanicOnErr(ts.API().Encode(testTx.Block)), + preSubscribeFunc: func() { + // we need to add the block to the nodebridge, so that it is available + // for the TransactionsIncludedBlockTopic + ts.MockAddBlock(testTx.BlockID, testTx.Block) + + // we also need to add the first output to the nodebridge, so that it is available. + // this is also used by the TransactionsIncludedBlockTopic to get the blockID of the block containing the transaction of that output + ts.MockAddOutput(testTx.OutputID, testTx.Output) + }, + postSubscribeFunc: func() { + ts.ReceiveAcceptedTransaction(&nodebridge.AcceptedTransaction{ + API: ts.API(), + Slot: testTx.BlockID.Slot(), + TransactionID: testTx.TransactionID, + // the consumed input + Consumed: []*nodebridge.Output{ + ts.NewSpentNodeBridgeOutputFromTransaction(tpkg.RandBlockID(), testTx.ConsumedOutputCreationTransaction, testTx.BlockID.Slot(), testTx.TransactionID), + }, + // the created output + Created: []*nodebridge.Output{ + ts.NewNodeBridgeOutputFromTransaction(testTx.BlockID, testTx.Transaction), + }, + }) + }, + } + }(), + + // ok - Basic block with tagged data - TopicBlockMetadata + func() *test { + blockMetadataResponse := &api.BlockMetadataResponse{ + BlockID: tpkg.RandBlockID(), + BlockState: api.BlockStateAccepted, + BlockFailureReason: api.BlockFailureNone, + TransactionMetadata: &api.TransactionMetadataResponse{ + TransactionID: tpkg.RandTransactionID(), + TransactionState: api.TransactionStateFailed, + TransactionFailureReason: api.TxFailureBICInputInvalid, + }, + } + + return &test{ + name: "ok - Basic block with tagged data - TopicBlockMetadata", + topics: []*testTopic{ + { + topic: mqtt.GetTopicBlockMetadata(blockMetadataResponse.BlockID), + isPollingTarget: true, + isEventTarget: true, + }, + }, + topicsIgnore: []string{ + mqtt.TopicBlockMetadataAccepted, + }, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(blockMetadataResponse)), + rawTarget: lo.PanicOnErr(ts.API().Encode(blockMetadataResponse)), + preSubscribeFunc: func() { + ts.MockAddBlockMetadata(blockMetadataResponse.BlockID, blockMetadataResponse) + }, + postSubscribeFunc: func() { + ts.ReceiveAcceptedBlock(lo.PanicOnErr(inx.WrapBlockMetadata(blockMetadataResponse))) + }, + } + }(), + + // ok - Basic block with tagged data - TopicBlockMetadataAccepted + func() *test { + blockMetadataResponse := &api.BlockMetadataResponse{ + BlockID: tpkg.RandBlockID(), + BlockState: api.BlockStateAccepted, + BlockFailureReason: api.BlockFailureNone, + TransactionMetadata: &api.TransactionMetadataResponse{ + TransactionID: tpkg.RandTransactionID(), + TransactionState: api.TransactionStateFailed, + TransactionFailureReason: api.TxFailureBICInputInvalid, + }, + } + + return &test{ + name: "ok - Basic block with tagged data - TopicBlockMetadataAccepted", + topics: []*testTopic{ + { + topic: mqtt.TopicBlockMetadataAccepted, + isPollingTarget: false, + isEventTarget: true, + }, + }, + topicsIgnore: []string{ + mqtt.GetTopicBlockMetadata(blockMetadataResponse.BlockID), + }, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(blockMetadataResponse)), + rawTarget: lo.PanicOnErr(ts.API().Encode(blockMetadataResponse)), + postSubscribeFunc: func() { + ts.ReceiveAcceptedBlock(lo.PanicOnErr(inx.WrapBlockMetadata(blockMetadataResponse))) + }, + } + }(), + + // ok - Basic block with tagged data - TopicBlockMetadataConfirmed + func() *test { + blockMetadataResponse := &api.BlockMetadataResponse{ + BlockID: tpkg.RandBlockID(), + BlockState: api.BlockStateAccepted, + BlockFailureReason: api.BlockFailureNone, + TransactionMetadata: &api.TransactionMetadataResponse{ + TransactionID: tpkg.RandTransactionID(), + TransactionState: api.TransactionStateFailed, + TransactionFailureReason: api.TxFailureBICInputInvalid, + }, + } + + return &test{ + name: "ok - Basic block with tagged data - TopicBlockMetadataConfirmed", + topics: []*testTopic{ + { + topic: mqtt.TopicBlockMetadataConfirmed, + isPollingTarget: false, + isEventTarget: true, + }, + }, + topicsIgnore: []string{ + mqtt.GetTopicBlockMetadata(blockMetadataResponse.BlockID), + }, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(blockMetadataResponse)), + rawTarget: lo.PanicOnErr(ts.API().Encode(blockMetadataResponse)), + postSubscribeFunc: func() { + ts.ReceiveConfirmedBlock(lo.PanicOnErr(inx.WrapBlockMetadata(blockMetadataResponse))) + }, + } + }(), + + // ok - TopicOutputs + func() *test { + testTx := ts.NewTestTransaction(true) + + return &test{ + name: "ok - TopicOutputs", + topics: []*testTopic{ + { + topic: mqtt.GetTopicOutput(testTx.OutputID), + isPollingTarget: true, + isEventTarget: true, + }, + }, + topicsIgnore: []string{ + mqtt.GetTopicTransactionsIncludedBlock(testTx.TransactionID), + mqtt.GetTopicOutput(testTx.ConsumedOutputID), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAny, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAddress, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + }, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(testTx.OutputWithMetadataResponse)), + rawTarget: lo.PanicOnErr(ts.API().Encode(testTx.OutputWithMetadataResponse)), + preSubscribeFunc: func() { + output := ts.NewNodeBridgeOutputFromOutputWithMetadata(testTx.OutputWithMetadataResponse) + ts.MockAddOutput(testTx.OutputID, output) + }, + postSubscribeFunc: func() { + ts.ReceiveAcceptedTransaction(&nodebridge.AcceptedTransaction{ + API: ts.API(), + Slot: testTx.BlockID.Slot(), + TransactionID: testTx.TransactionID, + // the consumed input + Consumed: []*nodebridge.Output{ + ts.NewSpentNodeBridgeOutputFromTransaction(tpkg.RandBlockID(), testTx.ConsumedOutputCreationTransaction, testTx.BlockID.Slot(), testTx.TransactionID), + }, + // the created output + Created: []*nodebridge.Output{ + ts.NewNodeBridgeOutputFromOutputWithMetadata(testTx.OutputWithMetadataResponse), + }, + }) + }, + } + }(), + + // ok - TopicAccountOutputs + func() *test { + accountOutput := tpkg.RandOutput(iotago.OutputAccount).(*iotago.AccountOutput) + testTx := ts.NewTestTransaction(true, tpkg.WithOutputs(iotago.Outputs[iotago.TxEssenceOutput]{ + accountOutput, + })) + + return &test{ + name: "ok - TopicAccountOutputs", + topics: []*testTopic{ + { + topic: mqtt.GetTopicAccountOutputs(accountOutput.AccountID, ts.API().ProtocolParameters().Bech32HRP()), + isPollingTarget: false, + isEventTarget: true, + }, + }, + topicsIgnore: []string{ + mqtt.GetTopicTransactionsIncludedBlock(testTx.TransactionID), + mqtt.GetTopicOutput(testTx.ConsumedOutputID), + mqtt.GetTopicOutput(testTx.OutputID), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAny, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAddress, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + }, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(testTx.OutputWithMetadataResponse)), + rawTarget: lo.PanicOnErr(ts.API().Encode(testTx.OutputWithMetadataResponse)), + postSubscribeFunc: func() { + ts.ReceiveAcceptedTransaction(&nodebridge.AcceptedTransaction{ + API: ts.API(), + Slot: testTx.BlockID.Slot(), + TransactionID: testTx.TransactionID, + // the consumed input + Consumed: []*nodebridge.Output{ + ts.NewSpentNodeBridgeOutputFromTransaction(tpkg.RandBlockID(), testTx.ConsumedOutputCreationTransaction, testTx.BlockID.Slot(), testTx.TransactionID), + }, + // the created output + Created: []*nodebridge.Output{ + ts.NewNodeBridgeOutputFromOutputWithMetadata(testTx.OutputWithMetadataResponse), + }, + }) + }, + } + }(), + + // ok - TopicAnchorOutputs + func() *test { + anchorOutput := tpkg.RandOutput(iotago.OutputAnchor).(*iotago.AnchorOutput) + testTx := ts.NewTestTransaction(true, tpkg.WithOutputs(iotago.Outputs[iotago.TxEssenceOutput]{ + anchorOutput, + })) + + return &test{ + name: "ok - TopicAnchorOutputs", + topics: []*testTopic{ + { + topic: mqtt.GetTopicAnchorOutputs(anchorOutput.AnchorID, ts.API().ProtocolParameters().Bech32HRP()), + isPollingTarget: false, + isEventTarget: true, + }, + }, + topicsIgnore: []string{ + mqtt.GetTopicTransactionsIncludedBlock(testTx.TransactionID), + mqtt.GetTopicOutput(testTx.ConsumedOutputID), + mqtt.GetTopicOutput(testTx.OutputID), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAny, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAddress, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionStateController, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionGovernor, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + }, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(testTx.OutputWithMetadataResponse)), + rawTarget: lo.PanicOnErr(ts.API().Encode(testTx.OutputWithMetadataResponse)), + postSubscribeFunc: func() { + ts.ReceiveAcceptedTransaction(&nodebridge.AcceptedTransaction{ + API: ts.API(), + Slot: testTx.BlockID.Slot(), + TransactionID: testTx.TransactionID, + // the consumed input + Consumed: []*nodebridge.Output{ + ts.NewSpentNodeBridgeOutputFromTransaction(tpkg.RandBlockID(), testTx.ConsumedOutputCreationTransaction, testTx.BlockID.Slot(), testTx.TransactionID), + }, + // the created output + Created: []*nodebridge.Output{ + ts.NewNodeBridgeOutputFromOutputWithMetadata(testTx.OutputWithMetadataResponse), + }, + }) + }, + } + }(), + + // ok - TopicFoundryOutputs + func() *test { + foundryOutput := tpkg.RandOutput(iotago.OutputFoundry).(*iotago.FoundryOutput) + testTx := ts.NewTestTransaction(true, tpkg.WithOutputs(iotago.Outputs[iotago.TxEssenceOutput]{ + foundryOutput, + })) + + return &test{ + name: "ok - TopicFoundryOutputs", + topics: []*testTopic{ + { + topic: mqtt.GetTopicFoundryOutputs(lo.PanicOnErr(foundryOutput.FoundryID())), + isPollingTarget: false, + isEventTarget: true, + }, + }, + topicsIgnore: []string{ + mqtt.GetTopicTransactionsIncludedBlock(testTx.TransactionID), + mqtt.GetTopicOutput(testTx.ConsumedOutputID), + mqtt.GetTopicOutput(testTx.OutputID), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAny, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAddress, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionImmutableAccount, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + }, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(testTx.OutputWithMetadataResponse)), + rawTarget: lo.PanicOnErr(ts.API().Encode(testTx.OutputWithMetadataResponse)), + postSubscribeFunc: func() { + ts.ReceiveAcceptedTransaction(&nodebridge.AcceptedTransaction{ + API: ts.API(), + Slot: testTx.BlockID.Slot(), + TransactionID: testTx.TransactionID, + // the consumed input + Consumed: []*nodebridge.Output{ + ts.NewSpentNodeBridgeOutputFromTransaction(tpkg.RandBlockID(), testTx.ConsumedOutputCreationTransaction, testTx.BlockID.Slot(), testTx.TransactionID), + }, + // the created output + Created: []*nodebridge.Output{ + ts.NewNodeBridgeOutputFromOutputWithMetadata(testTx.OutputWithMetadataResponse), + }, + }) + }, + } + }(), + + // ok - TopicNFTOutputs + func() *test { + nftOutput := tpkg.RandOutput(iotago.OutputNFT).(*iotago.NFTOutput) + testTx := ts.NewTestTransaction(true, tpkg.WithOutputs(iotago.Outputs[iotago.TxEssenceOutput]{ + nftOutput, + })) + + return &test{ + name: "ok - TopicNFTOutputs", + topics: []*testTopic{ + { + topic: mqtt.GetTopicNFTOutputs(nftOutput.NFTID, ts.API().ProtocolParameters().Bech32HRP()), + isPollingTarget: false, + isEventTarget: true, + }, + }, + topicsIgnore: []string{ + mqtt.GetTopicTransactionsIncludedBlock(testTx.TransactionID), + mqtt.GetTopicOutput(testTx.ConsumedOutputID), + mqtt.GetTopicOutput(testTx.OutputID), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAny, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAddress, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + }, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(testTx.OutputWithMetadataResponse)), + rawTarget: lo.PanicOnErr(ts.API().Encode(testTx.OutputWithMetadataResponse)), + postSubscribeFunc: func() { + ts.ReceiveAcceptedTransaction(&nodebridge.AcceptedTransaction{ + API: ts.API(), + Slot: testTx.BlockID.Slot(), + TransactionID: testTx.TransactionID, + // the consumed input + Consumed: []*nodebridge.Output{ + ts.NewSpentNodeBridgeOutputFromTransaction(tpkg.RandBlockID(), testTx.ConsumedOutputCreationTransaction, testTx.BlockID.Slot(), testTx.TransactionID), + }, + // the created output + Created: []*nodebridge.Output{ + ts.NewNodeBridgeOutputFromOutputWithMetadata(testTx.OutputWithMetadataResponse), + }, + }) + }, + } + }(), + + // ok - TopicDelegationOutputs + func() *test { + delegationOutput := tpkg.RandOutput(iotago.OutputDelegation).(*iotago.DelegationOutput) + testTx := ts.NewTestTransaction(true, tpkg.WithOutputs(iotago.Outputs[iotago.TxEssenceOutput]{ + delegationOutput, + })) + + return &test{ + name: "ok - TopicDelegationOutputs", + topics: []*testTopic{ + { + topic: mqtt.GetTopicDelegationOutputs(delegationOutput.DelegationID), + isPollingTarget: false, + isEventTarget: true, + }, + }, + topicsIgnore: []string{ + mqtt.GetTopicTransactionsIncludedBlock(testTx.TransactionID), + mqtt.GetTopicOutput(testTx.ConsumedOutputID), + mqtt.GetTopicOutput(testTx.OutputID), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAny, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAddress, testTx.OwnerAddress, ts.API().ProtocolParameters().Bech32HRP()), + }, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(testTx.OutputWithMetadataResponse)), + rawTarget: lo.PanicOnErr(ts.API().Encode(testTx.OutputWithMetadataResponse)), + postSubscribeFunc: func() { + ts.ReceiveAcceptedTransaction(&nodebridge.AcceptedTransaction{ + API: ts.API(), + Slot: testTx.BlockID.Slot(), + TransactionID: testTx.TransactionID, + // the consumed input + Consumed: []*nodebridge.Output{ + ts.NewSpentNodeBridgeOutputFromTransaction(tpkg.RandBlockID(), testTx.ConsumedOutputCreationTransaction, testTx.BlockID.Slot(), testTx.TransactionID), + }, + // the created output + Created: []*nodebridge.Output{ + ts.NewNodeBridgeOutputFromOutputWithMetadata(testTx.OutputWithMetadataResponse), + }, + }) + }, + } + }(), + + // ok - TopicOutputsByUnlockConditionAndAddress - Address/StorageReturn/Expiration + func() *test { + unlockAddress := tpkg.RandEd25519Address() + returnAddress := tpkg.RandEd25519Address() + + basicOutput := &iotago.BasicOutput{ + Amount: 1337, + Mana: 1337, + UnlockConditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{ + Address: unlockAddress, + }, + &iotago.StorageDepositReturnUnlockCondition{ + ReturnAddress: returnAddress, + Amount: 1337, + }, + &iotago.ExpirationUnlockCondition{ + ReturnAddress: returnAddress, + Slot: 1337, + }, + }, + Features: iotago.BasicOutputFeatures{}, + } + + testTx := ts.NewTestTransaction(false, tpkg.WithOutputs(iotago.Outputs[iotago.TxEssenceOutput]{ + basicOutput, + })) + + return &test{ + name: "ok - TopicOutputsByUnlockConditionAndAddress - Address/StorageReturn/Expiration", + topics: []*testTopic{ + { + topic: mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAny, unlockAddress, ts.API().ProtocolParameters().Bech32HRP()), + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAny, returnAddress, ts.API().ProtocolParameters().Bech32HRP()), + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAddress, unlockAddress, ts.API().ProtocolParameters().Bech32HRP()), + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionStorageReturn, returnAddress, ts.API().ProtocolParameters().Bech32HRP()), + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionExpiration, returnAddress, ts.API().ProtocolParameters().Bech32HRP()), + isPollingTarget: false, + isEventTarget: true, + }, + }, + topicsIgnore: []string{ + mqtt.GetTopicTransactionsIncludedBlock(testTx.TransactionID), + mqtt.GetTopicOutput(testTx.ConsumedOutputID), + mqtt.GetTopicOutput(testTx.OutputID), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAny, testTx.SenderAddress, ts.API().ProtocolParameters().Bech32HRP()), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAddress, testTx.SenderAddress, ts.API().ProtocolParameters().Bech32HRP()), + }, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(testTx.OutputWithMetadataResponse)), + rawTarget: lo.PanicOnErr(ts.API().Encode(testTx.OutputWithMetadataResponse)), + postSubscribeFunc: func() { + ts.ReceiveAcceptedTransaction(&nodebridge.AcceptedTransaction{ + API: ts.API(), + Slot: testTx.BlockID.Slot(), + TransactionID: testTx.TransactionID, + // the consumed input + Consumed: []*nodebridge.Output{ + ts.NewSpentNodeBridgeOutputFromTransaction(tpkg.RandBlockID(), testTx.ConsumedOutputCreationTransaction, testTx.BlockID.Slot(), testTx.TransactionID), + }, + // the created output + Created: []*nodebridge.Output{ + ts.NewNodeBridgeOutputFromOutputWithMetadata(testTx.OutputWithMetadataResponse), + }, + }) + }, + } + }(), + + // ok - TopicOutputsByUnlockConditionAndAddress - StateController/Governor + func() *test { + anchorOutput := tpkg.RandOutput(iotago.OutputAnchor).(*iotago.AnchorOutput) + + // we want to have different addresses for the state controller and governor to check the "any" topic + anchorOutput.UnlockConditionSet().GovernorAddress().Address = tpkg.RandAddress(iotago.AddressEd25519) + + stateControllerAddress := anchorOutput.UnlockConditionSet().StateControllerAddress().Address + governorAddress := anchorOutput.UnlockConditionSet().GovernorAddress().Address + + testTx := ts.NewTestTransaction(false, tpkg.WithOutputs(iotago.Outputs[iotago.TxEssenceOutput]{ + anchorOutput, + })) + + return &test{ + name: "ok - TopicOutputsByUnlockConditionAndAddress - StateController/Governor", + topics: []*testTopic{ + { + topic: mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAny, stateControllerAddress, ts.API().ProtocolParameters().Bech32HRP()), + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAny, governorAddress, ts.API().ProtocolParameters().Bech32HRP()), + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionStateController, stateControllerAddress, ts.API().ProtocolParameters().Bech32HRP()), + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionGovernor, governorAddress, ts.API().ProtocolParameters().Bech32HRP()), + isPollingTarget: false, + isEventTarget: true, + }, + }, + topicsIgnore: []string{ + mqtt.GetTopicTransactionsIncludedBlock(testTx.TransactionID), + mqtt.GetTopicOutput(testTx.ConsumedOutputID), + mqtt.GetTopicOutput(testTx.OutputID), + mqtt.GetTopicAnchorOutputs(anchorOutput.AnchorID, ts.API().ProtocolParameters().Bech32HRP()), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAny, testTx.SenderAddress, ts.API().ProtocolParameters().Bech32HRP()), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAddress, testTx.SenderAddress, ts.API().ProtocolParameters().Bech32HRP()), + }, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(testTx.OutputWithMetadataResponse)), + rawTarget: lo.PanicOnErr(ts.API().Encode(testTx.OutputWithMetadataResponse)), + postSubscribeFunc: func() { + ts.ReceiveAcceptedTransaction(&nodebridge.AcceptedTransaction{ + API: ts.API(), + Slot: testTx.BlockID.Slot(), + TransactionID: testTx.TransactionID, + // the consumed input + Consumed: []*nodebridge.Output{ + ts.NewSpentNodeBridgeOutputFromTransaction(tpkg.RandBlockID(), testTx.ConsumedOutputCreationTransaction, testTx.BlockID.Slot(), testTx.TransactionID), + }, + // the created output + Created: []*nodebridge.Output{ + ts.NewNodeBridgeOutputFromOutputWithMetadata(testTx.OutputWithMetadataResponse), + }, + }) + }, + } + }(), + + // ok - TopicOutputsByUnlockConditionAndAddress - ImmutableAccount + func() *test { + foundryOutput := tpkg.RandOutput(iotago.OutputFoundry).(*iotago.FoundryOutput) + immutableAccountAddress := foundryOutput.UnlockConditionSet().ImmutableAccount().Address + + testTx := ts.NewTestTransaction(false, tpkg.WithOutputs(iotago.Outputs[iotago.TxEssenceOutput]{ + foundryOutput, + })) + + return &test{ + name: "ok - TopicOutputsByUnlockConditionAndAddress - ImmutableAccount", + topics: []*testTopic{ + { + topic: mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAny, immutableAccountAddress, ts.API().ProtocolParameters().Bech32HRP()), + isPollingTarget: false, + isEventTarget: true, + }, + { + topic: mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionImmutableAccount, immutableAccountAddress, ts.API().ProtocolParameters().Bech32HRP()), + isPollingTarget: false, + isEventTarget: true, + }, + }, + topicsIgnore: []string{ + mqtt.GetTopicTransactionsIncludedBlock(testTx.TransactionID), + mqtt.GetTopicOutput(testTx.ConsumedOutputID), + mqtt.GetTopicOutput(testTx.OutputID), + mqtt.GetTopicFoundryOutputs(lo.PanicOnErr(foundryOutput.FoundryID())), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAny, testTx.SenderAddress, ts.API().ProtocolParameters().Bech32HRP()), + mqtt.GetTopicOutputsByUnlockConditionAndAddress(mqtt.UnlockConditionAddress, testTx.SenderAddress, ts.API().ProtocolParameters().Bech32HRP()), + }, + jsonTarget: lo.PanicOnErr(ts.API().JSONEncode(testTx.OutputWithMetadataResponse)), + rawTarget: lo.PanicOnErr(ts.API().Encode(testTx.OutputWithMetadataResponse)), + postSubscribeFunc: func() { + ts.ReceiveAcceptedTransaction(&nodebridge.AcceptedTransaction{ + API: ts.API(), + Slot: testTx.BlockID.Slot(), + TransactionID: testTx.TransactionID, + // the consumed input + Consumed: []*nodebridge.Output{ + ts.NewSpentNodeBridgeOutputFromTransaction(tpkg.RandBlockID(), testTx.ConsumedOutputCreationTransaction, testTx.BlockID.Slot(), testTx.TransactionID), + }, + // the created output + Created: []*nodebridge.Output{ + ts.NewNodeBridgeOutputFromOutputWithMetadata(testTx.OutputWithMetadataResponse), + }, + }) + }, + } + }(), + } + + for _, test := range tests { + + t.Run(test.name, func(t *testing.T) { + ts.Reset() + + ts.MQTTClientConnect("client1") + defer ts.MQTTClientDisconnect("client1") + + topicsReceived := make(map[string]struct{}) + subscriptionDone := false + + // wrap it in a function to avoid the topic variable to be overwritten by the loop + subscribeToMQTTTopic := func(topic *testTopic) { + ts.MQTTSubscribe("client1", topic.topic, func(topicName string, payloadBytes []byte) { + if !subscriptionDone && !topic.isPollingTarget { + // if the subscription is not done and it is not a polled target, we don't want to receive it. + return + } + + if subscriptionDone && !topic.isEventTarget { + // if the subscription is done and it is not an event target, we don't want to receive it. + return + } + + require.Equal(t, topic.topic, topicName, "topic mismatch") + require.Equal(t, test.jsonTarget, payloadBytes, "JSON payload mismatch") + topicsReceived[topicName] = struct{}{} + }) + + // also subscribe to the raw topics + topicNameRaw := topic.topic + "/raw" + ts.MQTTSubscribe("client1", topicNameRaw, func(topicName string, payloadBytes []byte) { + if !subscriptionDone && !topic.isPollingTarget { + // if the subscription is not done and it is not a polled target, we don't want to receive it. + return + } + + if subscriptionDone && !topic.isEventTarget { + // if the subscription is done and it is not an event target, we don't want to receive it. + return + } + + require.Equal(t, topicNameRaw, topicName, "topic mismatch") + require.Equal(t, test.rawTarget, payloadBytes, "raw payload mismatch") + topicsReceived[topicName] = struct{}{} + }) + } + + // check that we don't receive topics we don't subscribe to + receivedTopics := make(map[string]int) + ts.MQTTSetHasSubscribersCallback(func(topicName string) { + for _, ignoredTopic := range test.topicsIgnore { + if topicName == ignoredTopic { + return + } + if topicName == ignoredTopic+"/raw" { + return + } + } + + receivedTopics[topicName]++ + }) + + // collect all topics for later comparison with the received topics + collectedTopics := lo.Reduce(test.topics, func(collectedTopics map[string]int, topic *testTopic) map[string]int { + if topic.isPollingTarget { + collectedTopics[topic.topic]++ + collectedTopics[topic.topic+"/raw"]++ + } + if topic.isEventTarget { + collectedTopics[topic.topic]++ + collectedTopics[topic.topic+"/raw"]++ + } + + return collectedTopics + }, make(map[string]int)) + + // this step can be used to receive "polled" topics (inject the payload before subscribing) + if test.preSubscribeFunc != nil { + test.preSubscribeFunc() + } + + // subscribe to the topics + for _, testTopic := range test.topics { + subscribeToMQTTTopic(testTopic) + } + + // unfortunately we need to wait a bit here, because the MQTT broker is running in a separate goroutine + time.Sleep(50 * time.Millisecond) + + // everything we receive now is "event driven" + subscriptionDone = true + + // this step can be used to receive "event driven" topics + if test.postSubscribeFunc != nil { + test.postSubscribeFunc() + + // unfortunately we need to wait a bit here, because the MQTT broker is running in a separate goroutine + time.Sleep(50 * time.Millisecond) + } + + // check if all topics were received + for _, testTopic := range test.topics { + require.Containsf(t, topicsReceived, testTopic.topic, "topic not received: %s", testTopic.topic) + require.Containsf(t, topicsReceived, testTopic.topic+"/raw", "topic not received: %s", testTopic.topic+"/raw") + } + + // check that we don't receive topics we don't subscribe to + for topic, count := range receivedTopics { + if _, ok := collectedTopics[topic]; !ok { + require.Failf(t, "received topic that was not subscribed to", "topic: %s, count: %d", topic, count) + } + + require.Equalf(t, collectedTopics[topic], count, "topic count mismatch: %s", topic) + } + }) + } +} diff --git a/pkg/testsuite/nodebridge_mock.go b/pkg/testsuite/nodebridge_mock.go new file mode 100644 index 0000000..638b5e0 --- /dev/null +++ b/pkg/testsuite/nodebridge_mock.go @@ -0,0 +1,434 @@ +//nolint:revive,nilnil,structcheck,containedctx // skip linter for this package name +package testsuite + +import ( + "context" + "io" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/iotaledger/hive.go/runtime/event" + "github.com/iotaledger/inx-app/pkg/nodebridge" + inx "github.com/iotaledger/inx/go" + iotago "github.com/iotaledger/iota.go/v4" + iotaapi "github.com/iotaledger/iota.go/v4/api" + "github.com/iotaledger/iota.go/v4/nodeclient" +) + +type MockedNodeBridge struct { + t *testing.T + + events *nodebridge.Events + apiProvider iotago.APIProvider + + mockedLatestCommitment *nodebridge.Commitment + mockedLatestFinalizedCommitment *nodebridge.Commitment + + mockedBlocks map[iotago.BlockID]*iotago.Block + mockedBlockMetadata map[iotago.BlockID]*iotaapi.BlockMetadataResponse + mockedOutputs map[iotago.OutputID]*nodebridge.Output + + mockedStreamListenToBlocks *MockedStream[MockedBlock] + mockedStreamListenToAcceptedBlocks *MockedStream[inx.BlockMetadata] + mockedStreamListenToConfirmedBlocks *MockedStream[inx.BlockMetadata] + mockedStreamListenToCommitments *MockedStream[MockedCommitment] + mockedStreamListenToLedgerUpdates *MockedStream[nodebridge.LedgerUpdate] + mockedStreamListenToAcceptedTransactions *MockedStream[nodebridge.AcceptedTransaction] +} + +var _ nodebridge.NodeBridge = &MockedNodeBridge{} + +func NewMockedNodeBridge(t *testing.T, api iotago.API) *MockedNodeBridge { + t.Helper() + + return &MockedNodeBridge{ + t: t, + events: &nodebridge.Events{ + LatestCommitmentChanged: event.New1[*nodebridge.Commitment](), + LatestFinalizedCommitmentChanged: event.New1[*nodebridge.Commitment](), + }, + apiProvider: iotago.SingleVersionProvider(api), + mockedBlocks: make(map[iotago.BlockID]*iotago.Block), + mockedBlockMetadata: make(map[iotago.BlockID]*iotaapi.BlockMetadataResponse), + mockedOutputs: make(map[iotago.OutputID]*nodebridge.Output), + } +} + +// +// NodeBridge interface +// + +func (m *MockedNodeBridge) Events() *nodebridge.Events { + return m.events +} + +func (m *MockedNodeBridge) Connect(ctx context.Context, address string, maxConnectionAttempts uint) error { + panic("not implemented") +} + +func (m *MockedNodeBridge) Run(ctx context.Context) { + panic("not implemented") +} + +func (m *MockedNodeBridge) Client() inx.INXClient { + panic("not implemented") +} + +func (m *MockedNodeBridge) NodeConfig() *inx.NodeConfiguration { + panic("not implemented") +} + +func (m *MockedNodeBridge) APIProvider() iotago.APIProvider { + return m.apiProvider +} + +func (m *MockedNodeBridge) INXNodeClient() (*nodeclient.Client, error) { + panic("not implemented") +} + +func (m *MockedNodeBridge) Management(ctx context.Context) (nodeclient.ManagementClient, error) { + panic("not implemented") +} + +func (m *MockedNodeBridge) Indexer(ctx context.Context) (nodeclient.IndexerClient, error) { + panic("not implemented") +} + +func (m *MockedNodeBridge) EventAPI(ctx context.Context) (*nodeclient.EventAPIClient, error) { + panic("not implemented") +} + +func (m *MockedNodeBridge) BlockIssuer(ctx context.Context) (nodeclient.BlockIssuerClient, error) { + panic("not implemented") +} + +func (m *MockedNodeBridge) ReadIsCandidate(ctx context.Context, id iotago.AccountID, slot iotago.SlotIndex) (bool, error) { + panic("not implemented") +} + +func (m *MockedNodeBridge) ReadIsCommitteeMember(ctx context.Context, id iotago.AccountID, slot iotago.SlotIndex) (bool, error) { + panic("not implemented") +} + +func (m *MockedNodeBridge) ReadIsValidatorAccount(ctx context.Context, id iotago.AccountID, slot iotago.SlotIndex) (bool, error) { + panic("not implemented") +} + +func (m *MockedNodeBridge) RegisterAPIRoute(ctx context.Context, route string, bindAddress string, path string) error { + return nil +} + +func (m *MockedNodeBridge) UnregisterAPIRoute(ctx context.Context, route string) error { + return nil +} + +func (m *MockedNodeBridge) ActiveRootBlocks(ctx context.Context) (map[iotago.BlockID]iotago.CommitmentID, error) { + panic("not implemented") +} + +func (m *MockedNodeBridge) SubmitBlock(ctx context.Context, block *iotago.Block) (iotago.BlockID, error) { + panic("not implemented") +} + +func (m *MockedNodeBridge) Block(ctx context.Context, blockID iotago.BlockID) (*iotago.Block, error) { + if block, ok := m.mockedBlocks[blockID]; ok { + return block, nil + } + + return nil, status.Errorf(codes.NotFound, "block %s not found", blockID.ToHex()) +} + +func (m *MockedNodeBridge) BlockMetadata(ctx context.Context, blockID iotago.BlockID) (*iotaapi.BlockMetadataResponse, error) { + if blockMetadata, ok := m.mockedBlockMetadata[blockID]; ok { + return blockMetadata, nil + } + + return nil, status.Errorf(codes.NotFound, "metadata for block %s not found", blockID.ToHex()) +} + +func (m *MockedNodeBridge) ListenToBlocks(ctx context.Context, consumer func(block *iotago.Block, rawData []byte) error) error { + if m.mockedStreamListenToBlocks == nil { + require.FailNow(m.t, "ListenToBlocks mock not initialized") + } + + err := nodebridge.ListenToStream(ctx, m.mockedStreamListenToBlocks.receiverFunc(), func(block *MockedBlock) error { + return consumer(block.Block, block.RawBlockData) + }) + require.NoError(m.t, err, "ListenToBlocks failed") + + return nil +} + +func (m *MockedNodeBridge) ListenToAcceptedBlocks(ctx context.Context, consumer func(blockMetadata *iotaapi.BlockMetadataResponse) error) error { + if m.mockedStreamListenToAcceptedBlocks == nil { + require.FailNow(m.t, "ListenToAcceptedBlocks mock not initialized") + } + + err := nodebridge.ListenToStream(ctx, m.mockedStreamListenToAcceptedBlocks.receiverFunc(), func(inxBlockMetadata *inx.BlockMetadata) error { + blockMetadata, err := inxBlockMetadata.Unwrap() + if err != nil { + return err + } + + return consumer(blockMetadata) + }) + require.NoError(m.t, err, "ListenToAcceptedBlocks failed") + + return nil +} + +func (m *MockedNodeBridge) ListenToConfirmedBlocks(ctx context.Context, consumer func(blockMetadata *iotaapi.BlockMetadataResponse) error) error { + if m.mockedStreamListenToConfirmedBlocks == nil { + require.FailNow(m.t, "ListenToConfirmedBlocks mock not initialized") + } + + err := nodebridge.ListenToStream(ctx, m.mockedStreamListenToConfirmedBlocks.receiverFunc(), func(inxBlockMetadata *inx.BlockMetadata) error { + blockMetadata, err := inxBlockMetadata.Unwrap() + if err != nil { + return err + } + + return consumer(blockMetadata) + }) + require.NoError(m.t, err, "ListenToConfirmedBlocks failed") + + return nil +} + +// TransactionMetadata returns the transaction metadata for the given transaction ID. +func (m *MockedNodeBridge) TransactionMetadata(ctx context.Context, transactionID iotago.TransactionID) (*iotaapi.TransactionMetadataResponse, error) { + panic("not implemented") +} + +func (m *MockedNodeBridge) Output(ctx context.Context, outputID iotago.OutputID) (*nodebridge.Output, error) { + if output, ok := m.mockedOutputs[outputID]; ok { + return output, nil + } + + return nil, status.Errorf(codes.NotFound, "output %s not found", outputID.ToHex()) +} + +func (m *MockedNodeBridge) ForceCommitUntil(ctx context.Context, slot iotago.SlotIndex) error { + panic("not implemented") +} + +func (m *MockedNodeBridge) Commitment(ctx context.Context, slot iotago.SlotIndex) (*nodebridge.Commitment, error) { + panic("not implemented") +} + +func (m *MockedNodeBridge) CommitmentByID(ctx context.Context, id iotago.CommitmentID) (*nodebridge.Commitment, error) { + panic("not implemented") +} + +func (m *MockedNodeBridge) ListenToCommitments(ctx context.Context, startSlot, endSlot iotago.SlotIndex, consumer func(commitment *nodebridge.Commitment, rawData []byte) error) error { + if m.mockedStreamListenToCommitments == nil { + require.FailNow(m.t, "ListenToCommitments mock not initialized") + } + + err := nodebridge.ListenToStream(ctx, m.mockedStreamListenToCommitments.receiverFunc(), func(commitment *MockedCommitment) error { + return consumer(&nodebridge.Commitment{ + CommitmentID: commitment.CommitmentID, + Commitment: commitment.Commitment, + }, commitment.RawCommitmentData) + }) + require.NoError(m.t, err, "ListenToCommitments failed") + + return nil +} + +func (m *MockedNodeBridge) ListenToLedgerUpdates(ctx context.Context, startSlot, endSlot iotago.SlotIndex, consumer func(update *nodebridge.LedgerUpdate) error) error { + if m.mockedStreamListenToLedgerUpdates == nil { + require.FailNow(m.t, "ListenToLedgerUpdates mock not initialized") + } + + err := nodebridge.ListenToStream(ctx, m.mockedStreamListenToLedgerUpdates.receiverFunc(), consumer) + require.NoError(m.t, err, "ListenToLedgerUpdates failed") + + return nil +} + +func (m *MockedNodeBridge) ListenToAcceptedTransactions(ctx context.Context, consumer func(tx *nodebridge.AcceptedTransaction) error) error { + if m.mockedStreamListenToAcceptedTransactions == nil { + require.FailNow(m.t, "ListenToAcceptedTransactions mock not initialized") + } + + err := nodebridge.ListenToStream(ctx, m.mockedStreamListenToAcceptedTransactions.receiverFunc(), consumer) + require.NoError(m.t, err, "ListenToAcceptedTransactions failed") + + return nil +} + +func (m *MockedNodeBridge) NodeStatus() *inx.NodeStatus { + panic("not implemented") +} + +func (m *MockedNodeBridge) IsNodeHealthy() bool { + panic("not implemented") +} + +func (m *MockedNodeBridge) LatestCommitment() *nodebridge.Commitment { + return m.mockedLatestCommitment +} + +func (m *MockedNodeBridge) LatestFinalizedCommitment() *nodebridge.Commitment { + return m.mockedLatestFinalizedCommitment +} + +func (m *MockedNodeBridge) PruningEpoch() iotago.EpochIndex { + panic("not implemented") +} + +func (m *MockedNodeBridge) RequestTips(ctx context.Context, count uint32) (strong iotago.BlockIDs, weak iotago.BlockIDs, shallowLike iotago.BlockIDs, err error) { + panic("not implemented") +} + +// +// Mock functions +// + +func (m *MockedNodeBridge) MockClear() { + m.mockedBlocks = make(map[iotago.BlockID]*iotago.Block) + m.mockedBlockMetadata = make(map[iotago.BlockID]*iotaapi.BlockMetadataResponse) + m.mockedOutputs = make(map[iotago.OutputID]*nodebridge.Output) + + if m.mockedStreamListenToBlocks != nil { + m.mockedStreamListenToBlocks.Close() + m.mockedStreamListenToBlocks = nil + } + if m.mockedStreamListenToAcceptedBlocks != nil { + m.mockedStreamListenToAcceptedBlocks.Close() + m.mockedStreamListenToAcceptedBlocks = nil + } + if m.mockedStreamListenToConfirmedBlocks != nil { + m.mockedStreamListenToConfirmedBlocks.Close() + m.mockedStreamListenToConfirmedBlocks = nil + } + if m.mockedStreamListenToCommitments != nil { + m.mockedStreamListenToCommitments.Close() + m.mockedStreamListenToCommitments = nil + } + if m.mockedStreamListenToLedgerUpdates != nil { + m.mockedStreamListenToLedgerUpdates.Close() + m.mockedStreamListenToLedgerUpdates = nil + } + if m.mockedStreamListenToAcceptedTransactions != nil { + m.mockedStreamListenToAcceptedTransactions.Close() + m.mockedStreamListenToAcceptedTransactions = nil + } +} + +func (m *MockedNodeBridge) MockSetLatestCommitment(commitment *nodebridge.Commitment) { + m.mockedLatestCommitment = commitment +} + +func (m *MockedNodeBridge) MockSetLatestFinalizedCommitment(commitment *nodebridge.Commitment) { + m.mockedLatestFinalizedCommitment = commitment +} + +func (m *MockedNodeBridge) MockReceiveLatestCommitment(commitment *nodebridge.Commitment) { + m.mockedLatestCommitment = commitment + m.Events().LatestCommitmentChanged.Trigger(commitment) +} + +func (m *MockedNodeBridge) MockReceiveLatestFinalizedCommitment(commitment *nodebridge.Commitment) { + m.mockedLatestFinalizedCommitment = commitment + m.Events().LatestFinalizedCommitmentChanged.Trigger(commitment) +} + +func (m *MockedNodeBridge) MockAddBlock(blockID iotago.BlockID, block *iotago.Block) { + m.mockedBlocks[blockID] = block +} + +func (m *MockedNodeBridge) MockAddBlockMetadata(blockID iotago.BlockID, blockMetadata *iotaapi.BlockMetadataResponse) { + m.mockedBlockMetadata[blockID] = blockMetadata +} + +func (m *MockedNodeBridge) MockAddOutput(outputID iotago.OutputID, output *nodebridge.Output) { + m.mockedOutputs[outputID] = output +} + +type MockedBlock struct { + Block *iotago.Block + RawBlockData []byte +} + +type MockedCommitment struct { + CommitmentID iotago.CommitmentID + Commitment *iotago.Commitment + RawCommitmentData []byte +} + +func (m *MockedNodeBridge) MockListenToBlocks() *MockedStream[MockedBlock] { + m.mockedStreamListenToBlocks = InitMockedStream[MockedBlock]() + return m.mockedStreamListenToBlocks +} + +func (m *MockedNodeBridge) MockListenToAcceptedBlocks() *MockedStream[inx.BlockMetadata] { + m.mockedStreamListenToAcceptedBlocks = InitMockedStream[inx.BlockMetadata]() + return m.mockedStreamListenToAcceptedBlocks +} + +func (m *MockedNodeBridge) MockListenToConfirmedBlocks() *MockedStream[inx.BlockMetadata] { + m.mockedStreamListenToConfirmedBlocks = InitMockedStream[inx.BlockMetadata]() + return m.mockedStreamListenToConfirmedBlocks +} + +func (m *MockedNodeBridge) MockListenToCommitments() *MockedStream[MockedCommitment] { + m.mockedStreamListenToCommitments = InitMockedStream[MockedCommitment]() + return m.mockedStreamListenToCommitments +} + +func (m *MockedNodeBridge) MockListenToLedgerUpdates() *MockedStream[nodebridge.LedgerUpdate] { + m.mockedStreamListenToLedgerUpdates = InitMockedStream[nodebridge.LedgerUpdate]() + return m.mockedStreamListenToLedgerUpdates +} + +func (m *MockedNodeBridge) MockListenToAcceptedTransactions() *MockedStream[nodebridge.AcceptedTransaction] { + m.mockedStreamListenToAcceptedTransactions = InitMockedStream[nodebridge.AcceptedTransaction]() + return m.mockedStreamListenToAcceptedTransactions +} + +type MockedStream[T any] struct { + ctx context.Context + cancel context.CancelFunc + receiverChan chan *T +} + +func InitMockedStream[T any]() *MockedStream[T] { + ctx, cancel := context.WithCancel(context.Background()) + receiverChan := make(chan *T) + + return &MockedStream[T]{ + ctx: ctx, + cancel: cancel, + receiverChan: receiverChan, + } +} + +func (m *MockedStream[T]) receiverFunc() func() (*T, error) { + return func() (*T, error) { + select { + case <-m.ctx.Done(): + return nil, io.EOF + + case obj, ok := <-m.receiverChan: + if !ok { + return nil, io.EOF + } + + return obj, nil + } + } +} + +func (m *MockedStream[T]) Receive(obj *T) { + m.receiverChan <- obj +} + +func (m *MockedStream[T]) Close() { + m.cancel() + close(m.receiverChan) +} diff --git a/pkg/testsuite/testsuite.go b/pkg/testsuite/testsuite.go new file mode 100644 index 0000000..e090594 --- /dev/null +++ b/pkg/testsuite/testsuite.go @@ -0,0 +1,421 @@ +//nolint:contextcheck,forcetypeassert,exhaustive +package testsuite + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/iotaledger/hive.go/lo" + "github.com/iotaledger/hive.go/logger" + "github.com/iotaledger/hive.go/runtime/options" + "github.com/iotaledger/inx-app/pkg/nodebridge" + "github.com/iotaledger/inx-mqtt/pkg/mqtt" + inx "github.com/iotaledger/inx/go" + iotago "github.com/iotaledger/iota.go/v4" + "github.com/iotaledger/iota.go/v4/api" + "github.com/iotaledger/iota.go/v4/tpkg" +) + +type TestSuite struct { + T *testing.T + + api iotago.API + nodeBridge *MockedNodeBridge + broker *MockedBroker + server *mqtt.Server + + mockedStreamListenToBlocks *MockedStream[MockedBlock] + mockedStreamListenToAcceptedBlocks *MockedStream[inx.BlockMetadata] + mockedStreamListenToConfirmedBlocks *MockedStream[inx.BlockMetadata] + mockedStreamListenToCommitments *MockedStream[MockedCommitment] + mockedStreamListenToLedgerUpdates *MockedStream[nodebridge.LedgerUpdate] + mockedStreamListenToAcceptedTransactions *MockedStream[nodebridge.AcceptedTransaction] +} + +func NewTestSuite(t *testing.T) *TestSuite { + t.Helper() + + rootLogger, err := logger.NewRootLogger(logger.DefaultCfg) + require.NoError(t, err) + + api := iotago.V3API(iotago.NewV3ProtocolParameters()) + + bridge := NewMockedNodeBridge(t, api) + broker := NewMockedBroker(t) + server, err := mqtt.NewServer( + rootLogger.Named(t.Name()), + bridge, + broker, + nil, + ) + require.NoError(t, err) + + return &TestSuite{ + T: t, + api: api, + nodeBridge: bridge, + broker: broker, + server: server, + + mockedStreamListenToBlocks: bridge.MockListenToBlocks(), + mockedStreamListenToAcceptedBlocks: bridge.MockListenToAcceptedBlocks(), + mockedStreamListenToConfirmedBlocks: bridge.MockListenToConfirmedBlocks(), + mockedStreamListenToCommitments: bridge.MockListenToCommitments(), + mockedStreamListenToLedgerUpdates: bridge.MockListenToLedgerUpdates(), + mockedStreamListenToAcceptedTransactions: bridge.MockListenToAcceptedTransactions(), + } +} + +func (ts *TestSuite) Run(ctx context.Context) { + err := ts.server.Start(ctx) + require.NoError(ts.T, err) + + go func() { + <-ctx.Done() + + err = ts.server.Stop() + require.NoError(ts.T, err) + }() +} + +func (ts *TestSuite) API() iotago.API { + return ts.api +} + +func (ts *TestSuite) Reset() { + ts.nodeBridge.MockClear() + ts.broker.MockClear() + + ts.mockedStreamListenToBlocks = ts.nodeBridge.MockListenToBlocks() + ts.mockedStreamListenToAcceptedBlocks = ts.nodeBridge.MockListenToAcceptedBlocks() + ts.mockedStreamListenToConfirmedBlocks = ts.nodeBridge.MockListenToConfirmedBlocks() + ts.mockedStreamListenToCommitments = ts.nodeBridge.MockListenToCommitments() + ts.mockedStreamListenToLedgerUpdates = ts.nodeBridge.MockListenToLedgerUpdates() + ts.mockedStreamListenToAcceptedTransactions = ts.nodeBridge.MockListenToAcceptedTransactions() +} + +// +// NodeBridge +// + +func (ts *TestSuite) SetLatestCommitment(commitment *nodebridge.Commitment) { + ts.nodeBridge.MockSetLatestCommitment(commitment) +} + +func (ts *TestSuite) SetLatestFinalizedCommitment(commitment *nodebridge.Commitment) { + ts.nodeBridge.MockSetLatestFinalizedCommitment(commitment) +} + +func (ts *TestSuite) ReceiveLatestCommitment(commitment *nodebridge.Commitment) { + ts.nodeBridge.MockReceiveLatestCommitment(commitment) +} + +func (ts *TestSuite) ReceiveLatestFinalizedCommitment(commitment *nodebridge.Commitment) { + ts.nodeBridge.MockReceiveLatestFinalizedCommitment(commitment) +} + +func (ts *TestSuite) MockAddBlock(blockID iotago.BlockID, block *iotago.Block) { + ts.nodeBridge.MockAddBlock(blockID, block) +} + +func (ts *TestSuite) MockAddBlockMetadata(blockID iotago.BlockID, blockMetadata *api.BlockMetadataResponse) { + ts.nodeBridge.MockAddBlockMetadata(blockID, blockMetadata) +} + +func (ts *TestSuite) MockAddOutput(outputID iotago.OutputID, output *nodebridge.Output) { + ts.nodeBridge.MockAddOutput(outputID, output) +} + +func (ts *TestSuite) ReceiveBlock(block *MockedBlock) { + ts.mockedStreamListenToBlocks.Receive(block) +} + +func (ts *TestSuite) ReceiveAcceptedBlock(metadata *inx.BlockMetadata) { + ts.mockedStreamListenToAcceptedBlocks.Receive(metadata) +} + +func (ts *TestSuite) ReceiveConfirmedBlock(metadata *inx.BlockMetadata) { + ts.mockedStreamListenToConfirmedBlocks.Receive(metadata) +} + +func (ts *TestSuite) ReceiveCommitment(commitment *MockedCommitment) { + ts.mockedStreamListenToCommitments.Receive(commitment) +} + +func (ts *TestSuite) ReceiveLedgerUpdate(update *nodebridge.LedgerUpdate) { + ts.mockedStreamListenToLedgerUpdates.Receive(update) +} + +func (ts *TestSuite) ReceiveAcceptedTransaction(tx *nodebridge.AcceptedTransaction) { + ts.mockedStreamListenToAcceptedTransactions.Receive(tx) +} + +// +// MQTT Broker +// + +func (ts *TestSuite) MQTTSetHasSubscribersCallback(callback func(topic string)) { + ts.broker.MockSetHasSubscribersCallback(callback) +} + +func (ts *TestSuite) MQTTClientConnect(clientID string) { + ts.broker.MockClientConnected(clientID) +} + +func (ts *TestSuite) MQTTClientDisconnect(clientID string) { + ts.broker.MockClientDisconnected(clientID) +} + +func (ts *TestSuite) MQTTSubscribe(clientID string, topic string, callback func(topic string, payload []byte)) (unsubscribe func()) { + ts.broker.MockTopicSubscribed(clientID, topic, callback) + + return func() { + ts.broker.MockTopicUnsubscribed(clientID, topic) + } +} + +func (ts *TestSuite) MQTTUnsubscribe(clientID string, topic string) { + ts.broker.MockTopicUnsubscribed(clientID, topic) +} + +// +// Utility functions +// + +type TestTransaction struct { + ConsumedOutputCreationTransaction *iotago.Transaction + ConsumedOutputID iotago.OutputID + Transaction *iotago.Transaction + TransactionID iotago.TransactionID + Output *nodebridge.Output + OutputID iotago.OutputID + OutputWithMetadataResponse *api.OutputWithMetadataResponse + SenderAddress iotago.Address + OwnerAddress iotago.Address + Block *iotago.Block + BlockID iotago.BlockID +} + +func (ts *TestSuite) NewTestTransaction(fromSameAddress bool, opts ...options.Option[iotago.Transaction]) *TestTransaction { + // we need to create the transaction first to apply the options, so we can simplify the test by sending from the same address + transaction := &iotago.Transaction{ + API: ts.API(), + TransactionEssence: &iotago.TransactionEssence{ + NetworkID: ts.API().ProtocolParameters().NetworkID(), + CreationSlot: tpkg.RandSlot(), + ContextInputs: nil, + // we set those later + Inputs: iotago.TxEssenceInputs{}, + Allotments: nil, + Capabilities: nil, + Payload: nil, + }, + Outputs: iotago.Outputs[iotago.TxEssenceOutput]{ + tpkg.RandOutput(iotago.OutputBasic).(*iotago.BasicOutput), + }, + } + options.Apply(transaction, opts) + + createdOutput := transaction.Outputs[0] + + var ownerAddress iotago.Address + switch createdOutput.Type() { + case iotago.OutputAnchor: + ownerAddress = createdOutput.UnlockConditionSet().StateControllerAddress().Address + case iotago.OutputFoundry: + ownerAddress = createdOutput.UnlockConditionSet().ImmutableAccount().Address + default: + ownerAddress = createdOutput.UnlockConditionSet().Address().Address + } + + // simplify the test by sending from the same address (less topics to ignore) + consumedOutput := tpkg.RandOutput(iotago.OutputBasic).(*iotago.BasicOutput) + if fromSameAddress { + consumedOutput.UnlockConditionSet().Address().Address = ownerAddress + } + senderAddress := consumedOutput.UnlockConditionSet().Address().Address + + consumedOutputCreationTransaction := &iotago.Transaction{ + API: ts.API(), + TransactionEssence: &iotago.TransactionEssence{ + NetworkID: ts.API().ProtocolParameters().NetworkID(), + CreationSlot: tpkg.RandSlot(), + ContextInputs: nil, + Inputs: iotago.TxEssenceInputs{ + tpkg.RandUTXOInput(), + }, + Allotments: nil, + Capabilities: nil, + Payload: nil, + }, + Outputs: iotago.Outputs[iotago.TxEssenceOutput]{ + consumedOutput, + }, + } + consumedOutputID := iotago.OutputIDFromTransactionIDAndIndex(lo.PanicOnErr(consumedOutputCreationTransaction.ID()), 0) + + // now we can set the correct inputs + transaction.TransactionEssence.Inputs = iotago.TxEssenceInputs{ + consumedOutputID.UTXOInput(), + } + transactionID := lo.PanicOnErr(transaction.ID()) + + block := tpkg.RandBlock( + tpkg.RandBasicBlockBodyWithPayload(ts.API(), + tpkg.RandSignedTransactionWithTransaction(ts.API(), + transaction, + ), + ), ts.API(), iotago.Mana(500)) + blockID := block.MustID() + + output := ts.NewNodeBridgeOutputFromTransaction(blockID, transaction) + + return &TestTransaction{ + ConsumedOutputCreationTransaction: consumedOutputCreationTransaction, + ConsumedOutputID: consumedOutputID, + Transaction: transaction, + TransactionID: transactionID, + Output: output, + OutputID: output.OutputID, + OutputWithMetadataResponse: ts.NewOutputWithMetadataResponseFromTransaction(blockID, transaction), + SenderAddress: senderAddress, + OwnerAddress: ownerAddress, + Block: block, + BlockID: blockID, + } +} + +func (ts *TestSuite) NewOutputWithMetadataResponseFromTransaction(blockID iotago.BlockID, transaction *iotago.Transaction) *api.OutputWithMetadataResponse { + transactionID := lo.PanicOnErr(transaction.ID()) + output := transaction.Outputs[0] + outputID := iotago.OutputIDFromTransactionIDAndIndex(transactionID, 0) + outputIDProof := lo.PanicOnErr(iotago.OutputIDProofFromTransaction(transaction, 0)) + + return &api.OutputWithMetadataResponse{ + Output: output, + OutputIDProof: outputIDProof, + Metadata: &api.OutputMetadata{ + OutputID: outputID, + BlockID: blockID, + Included: &api.OutputInclusionMetadata{ + Slot: blockID.Slot(), + TransactionID: transactionID, + CommitmentID: tpkg.RandCommitmentID(), + }, + Spent: nil, + LatestCommitmentID: tpkg.RandCommitmentID(), + }, + } +} + +func (ts *TestSuite) NewSpentOutputWithMetadataResponseFromTransaction(creationBlockID iotago.BlockID, creationTx *iotago.Transaction, spendBlockID iotago.BlockID, spendTx *iotago.Transaction) *api.OutputWithMetadataResponse { + creationTransactionID := lo.PanicOnErr(creationTx.ID()) + spendTransactionID := lo.PanicOnErr(spendTx.ID()) + + output := creationTx.Outputs[0] + outputID := iotago.OutputIDFromTransactionIDAndIndex(creationTransactionID, 0) + outputIDProof := lo.PanicOnErr(iotago.OutputIDProofFromTransaction(creationTx, 0)) + + return &api.OutputWithMetadataResponse{ + Output: output, + OutputIDProof: outputIDProof, + Metadata: &api.OutputMetadata{ + OutputID: outputID, + BlockID: creationBlockID, + Included: &api.OutputInclusionMetadata{ + Slot: creationBlockID.Slot(), + TransactionID: creationTransactionID, + CommitmentID: tpkg.RandCommitmentID(), + }, + Spent: &api.OutputConsumptionMetadata{ + Slot: spendBlockID.Slot(), + TransactionID: spendTransactionID, + CommitmentID: tpkg.RandCommitmentID(), + }, + LatestCommitmentID: tpkg.RandCommitmentID(), + }, + } +} + +func (ts *TestSuite) NewNodeBridgeOutputFromTransaction(blockID iotago.BlockID, transaction *iotago.Transaction) *nodebridge.Output { + transactionID := lo.PanicOnErr(transaction.ID()) + output := transaction.Outputs[0] + outputID := iotago.OutputIDFromTransactionIDAndIndex(transactionID, 0) + outputIDProof := lo.PanicOnErr(iotago.OutputIDProofFromTransaction(transaction, 0)) + + return &nodebridge.Output{ + OutputID: outputID, + Output: output, + OutputIDProof: outputIDProof, + Metadata: &api.OutputMetadata{ + OutputID: outputID, + BlockID: blockID, + Included: &api.OutputInclusionMetadata{ + Slot: blockID.Slot(), + TransactionID: transactionID, + CommitmentID: tpkg.RandCommitmentID(), + }, + Spent: nil, + LatestCommitmentID: tpkg.RandCommitmentID(), + }, + RawOutputData: lo.PanicOnErr(ts.API().Encode(output)), + } +} + +func (ts *TestSuite) NewNodeBridgeOutputFromOutputWithMetadata(outputWithMetadata *api.OutputWithMetadataResponse) *nodebridge.Output { + return &nodebridge.Output{ + OutputID: outputWithMetadata.Metadata.OutputID, + Output: outputWithMetadata.Output, + OutputIDProof: outputWithMetadata.OutputIDProof, + Metadata: outputWithMetadata.Metadata, + RawOutputData: lo.PanicOnErr(ts.API().Encode(outputWithMetadata.Output)), + } +} + +func (ts *TestSuite) NewSpentNodeBridgeOutputFromTransaction(creationBlockID iotago.BlockID, creationTx *iotago.Transaction, spendSlot iotago.SlotIndex, spendTransactionID iotago.TransactionID) *nodebridge.Output { + creationTransactionID := lo.PanicOnErr(creationTx.ID()) + + output := creationTx.Outputs[0] + outputID := iotago.OutputIDFromTransactionIDAndIndex(creationTransactionID, 0) + outputIDProof := lo.PanicOnErr(iotago.OutputIDProofFromTransaction(creationTx, 0)) + + return &nodebridge.Output{ + OutputID: outputID, + Output: output, + OutputIDProof: outputIDProof, + Metadata: &api.OutputMetadata{ + OutputID: outputID, + BlockID: creationBlockID, + Included: &api.OutputInclusionMetadata{ + Slot: creationBlockID.Slot(), + TransactionID: creationTransactionID, + CommitmentID: tpkg.RandCommitmentID(), + }, + Spent: &api.OutputConsumptionMetadata{ + Slot: spendSlot, + TransactionID: spendTransactionID, + CommitmentID: tpkg.RandCommitmentID(), + }, + LatestCommitmentID: tpkg.RandCommitmentID(), + }, + RawOutputData: lo.PanicOnErr(ts.API().Encode(output)), + } +} + +func (ts *TestSuite) NewSpentNodeBridgeOutputFromOutputWithMetadata(outputWithMetadata *api.OutputWithMetadataResponse, spendSlot iotago.SlotIndex, spendTransactionID iotago.TransactionID) *nodebridge.Output { + outputWithMetadata.Metadata.Spent = &api.OutputConsumptionMetadata{ + Slot: spendSlot, + TransactionID: spendTransactionID, + CommitmentID: tpkg.RandCommitmentID(), + } + + return &nodebridge.Output{ + OutputID: outputWithMetadata.Metadata.OutputID, + Output: outputWithMetadata.Output, + OutputIDProof: outputWithMetadata.OutputIDProof, + Metadata: outputWithMetadata.Metadata, + RawOutputData: lo.PanicOnErr(ts.API().Encode(outputWithMetadata.Output)), + } +} diff --git a/scripts/gendoc.sh b/scripts/gendoc.sh new file mode 100755 index 0000000..9a5f390 --- /dev/null +++ b/scripts/gendoc.sh @@ -0,0 +1,11 @@ +#!/bin/bash +pushd ../tools/gendoc + +# determine current inx-mqtt version tag +commit_hash=$(git rev-parse --short HEAD) + +BUILD_LD_FLAGS="-s -w -X=github.com/iotaledger/inx-mqtt/components/app.Version=${commit_hash}" + +go run -ldflags "${BUILD_LD_FLAGS}" main.go + +popd diff --git a/tools/gendoc/go.mod b/tools/gendoc/go.mod index c21f285..20960f5 100644 --- a/tools/gendoc/go.mod +++ b/tools/gendoc/go.mod @@ -5,7 +5,7 @@ go 1.21 replace github.com/iotaledger/inx-mqtt => ../../ require ( - github.com/iotaledger/hive.go/app v0.0.0-20231027195901-620bd7470e42 + github.com/iotaledger/hive.go/app v0.0.0-20231130122510-e3dddb0214f0 github.com/iotaledger/hive.go/apputils v0.0.0-20230829152614-7afc7a4d89b3 github.com/iotaledger/inx-mqtt v0.0.0-00010101000000-000000000000 ) @@ -17,7 +17,7 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/eclipse/paho.mqtt.golang v1.4.3 // indirect - github.com/ethereum/go-ethereum v1.13.4 // indirect + github.com/ethereum/go-ethereum v1.13.5 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/fbiville/markdown-table-formatter v0.3.0 // indirect github.com/felixge/fgprof v0.9.3 // indirect @@ -26,34 +26,34 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-github v17.0.0+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect - github.com/gorilla/websocket v1.5.0 // indirect + github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect + github.com/gorilla/websocket v1.5.1 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect - github.com/holiman/uint256 v1.2.3 // indirect + github.com/holiman/uint256 v1.2.4 // indirect github.com/iancoleman/orderedmap v0.3.0 // indirect - github.com/iotaledger/hive.go/constraints v0.0.0-20231108050255-98e0fa35e936 // indirect - github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231108050255-98e0fa35e936 // indirect - github.com/iotaledger/hive.go/crypto v0.0.0-20231108050255-98e0fa35e936 // indirect - github.com/iotaledger/hive.go/ds v0.0.0-20231108044237-5731e50d3660 // indirect - github.com/iotaledger/hive.go/ierrors v0.0.0-20231108050255-98e0fa35e936 // indirect - github.com/iotaledger/hive.go/lo v0.0.0-20231108050255-98e0fa35e936 // indirect - github.com/iotaledger/hive.go/logger v0.0.0-20231027195901-620bd7470e42 // indirect - github.com/iotaledger/hive.go/runtime v0.0.0-20231108050255-98e0fa35e936 // indirect - github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231108050255-98e0fa35e936 // indirect - github.com/iotaledger/hive.go/stringify v0.0.0-20231108050255-98e0fa35e936 // indirect - github.com/iotaledger/hive.go/web v0.0.0-20230912172434-dc477e1f5140 // indirect - github.com/iotaledger/inx-app v1.0.0-rc.3.0.20231108104504-1445f545de82 // indirect - github.com/iotaledger/inx/go v1.0.0-rc.2.0.20231108104322-f301c3573998 // indirect - github.com/iotaledger/iota.go/v4 v4.0.0-20231108103955-bf75d703d8aa // indirect + github.com/iotaledger/hive.go/constraints v0.0.0-20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/hive.go/crypto v0.0.0-20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/hive.go/ds v0.0.0-20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/hive.go/ierrors v0.0.0-20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/hive.go/lo v0.0.0-20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/hive.go/logger v0.0.0-20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/hive.go/runtime v0.0.0-20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/hive.go/stringify v0.0.0-20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/hive.go/web v0.0.0-20231130122510-e3dddb0214f0 // indirect + github.com/iotaledger/inx-app v1.0.0-rc.3.0.20231201123347-1c44b3f24221 // indirect + github.com/iotaledger/inx/go v1.0.0-rc.2.0.20231201114846-3bb5c3fd5665 // indirect + github.com/iotaledger/iota.go/v4 v4.0.0-20231205153517-c03d459b5887 // indirect github.com/knadh/koanf v1.5.0 // indirect github.com/kr/text v0.2.0 // indirect - github.com/labstack/echo/v4 v4.11.2 // indirect - github.com/labstack/gommon v0.4.0 // indirect + github.com/labstack/echo/v4 v4.11.3 // indirect + github.com/labstack/gommon v0.4.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -61,7 +61,7 @@ require ( github.com/mr-tron/base58 v1.2.0 // indirect github.com/pasztorpisti/qs v0.0.0-20171216220353-8d6c33ee906c // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect - github.com/petermattis/goid v0.0.0-20230904192822-1876fd5063bc // indirect + github.com/petermattis/goid v0.0.0-20231126143041-f558c26febf5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.17.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect @@ -69,7 +69,7 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/rs/xid v1.5.0 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect - github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect @@ -77,13 +77,13 @@ require ( go.uber.org/dig v1.17.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sync v0.4.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect - golang.org/x/time v0.3.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.4.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/tools/gendoc/go.sum b/tools/gendoc/go.sum index 0bbaa9f..997cfcd 100644 --- a/tools/gendoc/go.sum +++ b/tools/gendoc/go.sum @@ -24,8 +24,6 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+ github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -63,9 +61,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/ethereum/go-ethereum v1.12.2 h1:eGHJ4ij7oyVqUQn48LBz3B7pvQ8sV0wGJiIE6gDq/6Y= -github.com/ethereum/go-ethereum v1.12.2/go.mod h1:1cRAEV+rp/xX0zraSCBnu9Py3HQ+geRMj3HdR+k0wfI= -github.com/ethereum/go-ethereum v1.13.4/go.mod h1:I0U5VewuuTzvBtVzKo7b3hJzDhXOUtn9mJW7SsIPB0Q= +github.com/ethereum/go-ethereum v1.13.5 h1:U6TCRciCqZRe4FPXmy1sMGxTfuk8P7u2UoinF3VbaFk= +github.com/ethereum/go-ethereum v1.13.5/go.mod h1:yMTu38GSuyxaYzQMViqNmQ1s3cE84abZexQmTgenWk0= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -74,11 +71,10 @@ github.com/fbiville/markdown-table-formatter v0.3.0 h1:PIm1UNgJrFs8q1htGTw+wnnNY github.com/fbiville/markdown-table-formatter v0.3.0/go.mod h1:q89TDtSEVDdTaufgSbfHpNVdPU/bmfvqNkrC5HagmLY= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= -github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -133,11 +129,11 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= -github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f h1:pDhu5sgp8yJlEF/g6osliIIpF9K4F5jvkULXa4daRDQ= -github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk= +github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= @@ -183,58 +179,43 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= -github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o= -github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= +github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= +github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= -github.com/iotaledger/hive.go/app v0.0.0-20230829152614-7afc7a4d89b3 h1:LeaRBaBnQmuu60yoAhdPJTv9w+YSsR323Fj1FTRCp2U= -github.com/iotaledger/hive.go/app v0.0.0-20230829152614-7afc7a4d89b3/go.mod h1:eiZgbcwTDZ7d9hEait2EAwAhixWhceW4MXmuVk2EcEw= -github.com/iotaledger/hive.go/app v0.0.0-20231027195901-620bd7470e42/go.mod h1:8ZbIKR84oQd/3iQ5eeT7xpudO9/ytzXP7veIYnk7Orc= +github.com/iotaledger/hive.go/app v0.0.0-20231130122510-e3dddb0214f0 h1:lB5jffAnqhssIPrMnZoiXttRpfymDLDGsW6vLfx0v94= +github.com/iotaledger/hive.go/app v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:+riYmeLApkLlj4+EpuJpEJAsj/KGfD7cqLGy7oTsPOM= github.com/iotaledger/hive.go/apputils v0.0.0-20230829152614-7afc7a4d89b3 h1:4aVJTc0KS77uEw0Tny4r0n1ORwcbAQDECaCclgf/6lE= github.com/iotaledger/hive.go/apputils v0.0.0-20230829152614-7afc7a4d89b3/go.mod h1:TZeAqieDu+xDOZp2e9+S+8pZp1PrfgcwLUnxmd8IgLU= -github.com/iotaledger/hive.go/constraints v0.0.0-20230829152614-7afc7a4d89b3 h1:n03Q+VxmhzDf072BM+73gU91lhiVXaQkQqo0hbKATwo= -github.com/iotaledger/hive.go/constraints v0.0.0-20230829152614-7afc7a4d89b3/go.mod h1:dOBOM2s4se3HcWefPe8sQLUalGXJ8yVXw58oK8jke3s= -github.com/iotaledger/hive.go/constraints v0.0.0-20231108050255-98e0fa35e936/go.mod h1:dOBOM2s4se3HcWefPe8sQLUalGXJ8yVXw58oK8jke3s= -github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20230829152614-7afc7a4d89b3 h1:050Lk+UXtoHcAeXpQmf2Qjoxzk4FPVZnr/EmdmBhXsY= -github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20230829152614-7afc7a4d89b3/go.mod h1:jn3TNmiNRIiQm/rS4VD+7wFHI2+UXABHvCA3PbQxBqI= -github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231108050255-98e0fa35e936/go.mod h1:CdixkrB7VdQzEDlVuwsxPtsiJL/WXrQgz3PELIqlLko= -github.com/iotaledger/hive.go/crypto v0.0.0-20230829152614-7afc7a4d89b3 h1:ljemi2tZRpZTuE53wzxB8lXrKMxNSDZkIggHn1kAGso= -github.com/iotaledger/hive.go/crypto v0.0.0-20230829152614-7afc7a4d89b3/go.mod h1:jP68na941d9uq7RtnA8aQ/FtIGRGz/51cU4uXrInQFU= -github.com/iotaledger/hive.go/crypto v0.0.0-20231108050255-98e0fa35e936/go.mod h1:OQ9EVTTQT1mkO/16BgwSIyQlAhEg+Cptud/yutevWsI= -github.com/iotaledger/hive.go/ds v0.0.0-20230829152614-7afc7a4d89b3 h1:L5INqiENOYTIOwKhL2oXztLdsW7KettZTlGGnCN3Vns= -github.com/iotaledger/hive.go/ds v0.0.0-20230829152614-7afc7a4d89b3/go.mod h1:G4azdfQMwAkSU5lHZNRAdGfRoLHNHF4+DfCudWTTKF8= -github.com/iotaledger/hive.go/ds v0.0.0-20231108044237-5731e50d3660/go.mod h1:RnYZNMRIXKt/SXVsFA18uUBNDZMQm0h5BykMXjv8/8A= -github.com/iotaledger/hive.go/ierrors v0.0.0-20230829152614-7afc7a4d89b3 h1:pMz5HyIGsp9VRB2gwqOgaEDhmlqbdvgS8G3tDu9PkkQ= -github.com/iotaledger/hive.go/ierrors v0.0.0-20230829152614-7afc7a4d89b3/go.mod h1:HcE8B5lP96enc/OALTb2/rIIi+yOLouRoHOKRclKmC8= -github.com/iotaledger/hive.go/ierrors v0.0.0-20231108050255-98e0fa35e936/go.mod h1:HcE8B5lP96enc/OALTb2/rIIi+yOLouRoHOKRclKmC8= -github.com/iotaledger/hive.go/lo v0.0.0-20230829152614-7afc7a4d89b3 h1:ZAg8B2w9Je/m0+JN+S7qW8bELfX1I2BjpNjLhU4IpQU= -github.com/iotaledger/hive.go/lo v0.0.0-20230829152614-7afc7a4d89b3/go.mod h1:/LERu5vqcessCqr40Wxmbx4x0bbymsK7GuL+TK/ckKo= -github.com/iotaledger/hive.go/lo v0.0.0-20231108050255-98e0fa35e936/go.mod h1:6Ee7i6b4tuTHuRYnPP8VUb0wr9XFI5qlqtnttBd9jRg= -github.com/iotaledger/hive.go/logger v0.0.0-20230829152614-7afc7a4d89b3 h1:JdCeAesfEyBSxx7nVp0zSL4rEGg2PtRtmL4qtxCPy7A= -github.com/iotaledger/hive.go/logger v0.0.0-20230829152614-7afc7a4d89b3/go.mod h1:sxqWRdZ1OOxwkxVczuGcW034Mpt2vFh5ebJHO++ZYeI= -github.com/iotaledger/hive.go/logger v0.0.0-20231027195901-620bd7470e42/go.mod h1:aBfAfIB2GO/IblhYt5ipCbyeL9bXSNeAwtYVA3hZaHg= -github.com/iotaledger/hive.go/runtime v0.0.0-20230829152614-7afc7a4d89b3 h1:fwJri2b4PXJJ19/W0iGjZSpu/vwK4QAytJ7PwDPNjMM= -github.com/iotaledger/hive.go/runtime v0.0.0-20230829152614-7afc7a4d89b3/go.mod h1:fXVyQ1MAwxe/EmjAnG8WcQqbzGk9EW/FsJ/n16H/f/w= -github.com/iotaledger/hive.go/runtime v0.0.0-20231108050255-98e0fa35e936/go.mod h1:DrZPvUvLarK8C2qb+3H2vdypp/MuhpQmB3iMJbDCr/Q= -github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20230829152614-7afc7a4d89b3 h1:WpL8R5ltMAf8rgksjBgdO2FXDa3zKN2R29lZ2C6pJGU= -github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20230829152614-7afc7a4d89b3/go.mod h1:IJgaaxbgKCsNat18jlJJEAxCY2oVYR3F30B+M4vJ89I= -github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231108050255-98e0fa35e936/go.mod h1:FoH3T6yKlZJp8xm8K+zsQiibSynp32v21CpWx8xkek8= -github.com/iotaledger/hive.go/stringify v0.0.0-20230829152614-7afc7a4d89b3 h1:iJ+AkAO6hj5tzoOvEFIfm67eG8g5UvvGYP+6xMA98sg= -github.com/iotaledger/hive.go/stringify v0.0.0-20230829152614-7afc7a4d89b3/go.mod h1:FTo/UWzNYgnQ082GI9QVM9HFDERqf9rw9RivNpqrnTs= -github.com/iotaledger/hive.go/stringify v0.0.0-20231108050255-98e0fa35e936/go.mod h1:FTo/UWzNYgnQ082GI9QVM9HFDERqf9rw9RivNpqrnTs= -github.com/iotaledger/hive.go/web v0.0.0-20230629181801-64c530ff9d15 h1:T9Wg7bMu8NWoVDyVoPXFVvVF4OE4ZNybs3pSObffn3U= -github.com/iotaledger/hive.go/web v0.0.0-20230629181801-64c530ff9d15/go.mod h1:A3LLvpa7mREy3eWZf+UsD1A1ujo4YZHK+e2IHvou+HQ= -github.com/iotaledger/hive.go/web v0.0.0-20230912172434-dc477e1f5140/go.mod h1:RVlTa6pmKvwpQZAhVKFaU9vNA13iognNVA63zdE6i6U= -github.com/iotaledger/inx-app v1.0.0-rc.3.0.20230906120020-eb566076caf6 h1:Sv517D+AVhRhI7GraXukobk0uFvawR+j+qRDf46btiI= -github.com/iotaledger/inx-app v1.0.0-rc.3.0.20230906120020-eb566076caf6/go.mod h1:C1CCtRn+VbKwIcPYzqg4gZn64in15Ka82ZTYeCCgjqI= -github.com/iotaledger/inx-app v1.0.0-rc.3.0.20231108104504-1445f545de82/go.mod h1:HVxkGPraMDTRudfG9AFN7Ga9gijp6skXB9TKNBc4KgI= -github.com/iotaledger/inx/go v1.0.0-rc.2.0.20230904035052-54cba612a7bb h1:sHaw5Nkdbz9t4SYqghN34ZSbvYHU/PhoYKBkkqpa0Kw= -github.com/iotaledger/inx/go v1.0.0-rc.2.0.20230904035052-54cba612a7bb/go.mod h1:B7gyJP6GshCSlEmY3CxEk5TZdsMs3UNz5U92hkFDdMs= -github.com/iotaledger/inx/go v1.0.0-rc.2.0.20231108104322-f301c3573998/go.mod h1:c+lBG3vgt2rgXHeOncK8hMllMwihTAtVbu790NslW2w= -github.com/iotaledger/iota.go/v4 v4.0.0-20230829160021-46cad51e89d1 h1:10hGLm62uQ2V2HgqCR6FV0fvgpXo5GqlL/SIJ/t4VmY= -github.com/iotaledger/iota.go/v4 v4.0.0-20230829160021-46cad51e89d1/go.mod h1:MM3RLtTEsfT6Wh0EhpgmzVO/HM0/NOw+E7+mnGTnyA0= -github.com/iotaledger/iota.go/v4 v4.0.0-20231108103955-bf75d703d8aa/go.mod h1:8iDORW4/e4NztyAGqjW07uSMjbhs7snbxw+81IWOczY= +github.com/iotaledger/hive.go/constraints v0.0.0-20231130122510-e3dddb0214f0 h1:hK4CDteuTMBA/ugMJusUJ7PJweWkf56z2ELNvnGB7fU= +github.com/iotaledger/hive.go/constraints v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:dOBOM2s4se3HcWefPe8sQLUalGXJ8yVXw58oK8jke3s= +github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231130122510-e3dddb0214f0 h1:K62beF6iGVwBrMJZA06dcXpqjlm9ZY21xqwq2oBQhVs= +github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231130122510-e3dddb0214f0/go.mod h1:CdixkrB7VdQzEDlVuwsxPtsiJL/WXrQgz3PELIqlLko= +github.com/iotaledger/hive.go/crypto v0.0.0-20231130122510-e3dddb0214f0 h1:F7H4BePrwtWwFXpRVZedU44zpbFks46pTrEFifBm+00= +github.com/iotaledger/hive.go/crypto v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:OQ9EVTTQT1mkO/16BgwSIyQlAhEg+Cptud/yutevWsI= +github.com/iotaledger/hive.go/ds v0.0.0-20231130122510-e3dddb0214f0 h1:TdO6KwA1uvQKG5uJ2ywDTLPzfXF7xGD3C0l1pWvSvZg= +github.com/iotaledger/hive.go/ds v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:JE8cbZSvzbB5TrwXibg6M0B7ck35YxF30ItHBzQRlgc= +github.com/iotaledger/hive.go/ierrors v0.0.0-20231130122510-e3dddb0214f0 h1:mP2pgiT4NufRBgATE3UuqgLNXBl4YGtWBQt3U8Mh6NU= +github.com/iotaledger/hive.go/ierrors v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:HcE8B5lP96enc/OALTb2/rIIi+yOLouRoHOKRclKmC8= +github.com/iotaledger/hive.go/lo v0.0.0-20231130122510-e3dddb0214f0 h1:DI78+2CRpWBfKzmXlEqJB6o4j0gl4KUIUwHojcdkZuc= +github.com/iotaledger/hive.go/lo v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:6Ee7i6b4tuTHuRYnPP8VUb0wr9XFI5qlqtnttBd9jRg= +github.com/iotaledger/hive.go/logger v0.0.0-20231130122510-e3dddb0214f0 h1:Vgz30mlavKHTGHzjnsYPJ2SPGwbFjUuFm/1KJtfRhyQ= +github.com/iotaledger/hive.go/logger v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:w1psHM2MuKsen1WdsPKrpqElYH7ZOQ+YdQIgJZg4HTo= +github.com/iotaledger/hive.go/runtime v0.0.0-20231130122510-e3dddb0214f0 h1:5YDrqQ8J+MIdICVrOMBwOIdzz6hILgQNLjrzYkC5yoI= +github.com/iotaledger/hive.go/runtime v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:DrZPvUvLarK8C2qb+3H2vdypp/MuhpQmB3iMJbDCr/Q= +github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231130122510-e3dddb0214f0 h1:UE9L3O12daz2pEp5TkFVo0rBvHYUwXWmpoWRJ1KRI0w= +github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231130122510-e3dddb0214f0/go.mod h1:FoH3T6yKlZJp8xm8K+zsQiibSynp32v21CpWx8xkek8= +github.com/iotaledger/hive.go/stringify v0.0.0-20231130122510-e3dddb0214f0 h1:cFJCQh5FBEePoWmc4dTZvCrKeJ+MaSzmP3jpn8VsSzQ= +github.com/iotaledger/hive.go/stringify v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:FTo/UWzNYgnQ082GI9QVM9HFDERqf9rw9RivNpqrnTs= +github.com/iotaledger/hive.go/web v0.0.0-20231130122510-e3dddb0214f0 h1:bIfWp0AOQXMv3VkDAOQwqvMYLz1Ila1Z2hqiY2RJ3Io= +github.com/iotaledger/hive.go/web v0.0.0-20231130122510-e3dddb0214f0/go.mod h1:L/CLz7skt9dvidhBOw2gmMGhmrUBHXlA0b3paugdsE4= +github.com/iotaledger/inx-app v1.0.0-rc.3.0.20231201123347-1c44b3f24221 h1:+ozrau44uPy2kYv2fuj2Wks8+VkXR62WB9zONOJgzdE= +github.com/iotaledger/inx-app v1.0.0-rc.3.0.20231201123347-1c44b3f24221/go.mod h1:6cLX3gnhP0WL+Q+mf3/rIqfACe5fWKVR8luPXWh2xiY= +github.com/iotaledger/inx/go v1.0.0-rc.2.0.20231201114846-3bb5c3fd5665 h1:XdhojOpZ0t0pJFyNO0zlBogSAUrhEI67eCpTC9H6sGM= +github.com/iotaledger/inx/go v1.0.0-rc.2.0.20231201114846-3bb5c3fd5665/go.mod h1:obK1N42oafGA7EH6zC4VX2fKh7jTa3WnIa9h1azfxq0= +github.com/iotaledger/iota.go/v4 v4.0.0-20231205153517-c03d459b5887 h1:i42HEMC0oWjKOy/v53sP45T8EHavhnk6BFpX1TRYUSI= +github.com/iotaledger/iota.go/v4 v4.0.0-20231205153517-c03d459b5887/go.mod h1:lCk9rhP3B5pX9BKhzR+Jobq4xPd+GHlqgF4Ga+eQfWA= github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -262,15 +243,13 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= -github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= -github.com/labstack/echo/v4 v4.11.2/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM= +github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= +github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk= +github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -278,14 +257,12 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -326,9 +303,8 @@ github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAv github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= -github.com/petermattis/goid v0.0.0-20230808133559-b036b712a89b h1:vab8deKC4QoIfm9fJM59iuNz1ELGsuLoYYpiF+pHiG8= -github.com/petermattis/goid v0.0.0-20230808133559-b036b712a89b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= -github.com/petermattis/goid v0.0.0-20230904192822-1876fd5063bc/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/petermattis/goid v0.0.0-20231126143041-f558c26febf5 h1:+qIP3OMrT7SN5kLnTcVEISPOMB/97RyAKTg1UWA738E= +github.com/petermattis/goid v0.0.0-20231126143041-f558c26febf5/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -342,28 +318,24 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= -github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= -github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= -github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= -github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= @@ -374,14 +346,16 @@ github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -402,7 +376,6 @@ github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e h1:IWllFTiDjjLIf2 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkTYKSv5m6MCKkOQlHwaShTMl3HjqSGW3XtVhXM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -412,8 +385,7 @@ go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dY go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI= -go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU= +go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= @@ -423,18 +395,18 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= -go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -461,9 +433,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -476,9 +447,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -509,17 +479,12 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -527,12 +492,11 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY= +golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -558,9 +522,8 @@ google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -570,8 +533,7 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= -google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=