diff --git a/.github/workflows/build-xmtpd.yml b/.github/workflows/build-xmtpd.yml index f77591df..3277c648 100644 --- a/.github/workflows/build-xmtpd.yml +++ b/.github/workflows/build-xmtpd.yml @@ -39,4 +39,4 @@ jobs: file: ./dev/docker/Dockerfile push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab29f22f..960d4e3c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,10 +10,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + submodules: recursive - uses: actions/setup-go@v3 with: go-version-file: go.mod - run: dev/docker/up + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + - run: dev/contracts/deploy-local - name: Run Tests run: | export GOPATH="${HOME}/go/" diff --git a/dev/contracts/deploy-local b/dev/contracts/deploy-local index 61cc9316..c3862a4e 100755 --- a/dev/contracts/deploy-local +++ b/dev/contracts/deploy-local @@ -3,6 +3,9 @@ source dev/contracts/.env +# Make sure the build directory exists +mkdir -p ./build + cd ./contracts # Deploy a contract and save the output (which includes the contract address) to a JSON file to be used in tests diff --git a/pkg/config/options.go b/pkg/config/options.go index dd082a42..c59d3b56 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -12,7 +12,7 @@ type ContractsOptions struct { RpcUrl string `long:"rpc-url" description:"Blockchain RPC URL"` NodesContractAddress string `long:"nodes-address" description:"Node contract address"` MessagesContractAddress string `long:"messages-address" description:"Message contract address"` - RefreshInterval time.Duration `long:"refresh-interval" description:"Refresh interval" default:"60s"` + RefreshInterval time.Duration `long:"refresh-interval" description:"Refresh interval for the nodes registry" default:"60s"` } type DbOptions struct { diff --git a/pkg/indexer/blockchain/client.go b/pkg/indexer/blockchain/client.go new file mode 100644 index 00000000..7699152c --- /dev/null +++ b/pkg/indexer/blockchain/client.go @@ -0,0 +1,11 @@ +package blockchain + +import ( + "context" + + "github.com/ethereum/go-ethereum/ethclient" +) + +func NewClient(ctx context.Context, rpcUrl string) (*ethclient.Client, error) { + return ethclient.DialContext(ctx, rpcUrl) +} diff --git a/pkg/indexer/blockchain/rpcLogStreamer.go b/pkg/indexer/blockchain/rpcLogStreamer.go index a01dcdcd..ec577883 100644 --- a/pkg/indexer/blockchain/rpcLogStreamer.go +++ b/pkg/indexer/blockchain/rpcLogStreamer.go @@ -26,11 +26,11 @@ type RpcLogStreamBuilder struct { // All the listeners contractConfigs []contractConfig logger *zap.Logger - rpcUrl string + ethclient *ethclient.Client } -func NewRpcLogStreamBuilder(rpcUrl string, logger *zap.Logger) *RpcLogStreamBuilder { - return &RpcLogStreamBuilder{rpcUrl: rpcUrl, logger: logger} +func NewRpcLogStreamBuilder(client *ethclient.Client, logger *zap.Logger) *RpcLogStreamBuilder { + return &RpcLogStreamBuilder{ethclient: client, logger: logger} } func (c *RpcLogStreamBuilder) ListenForContractEvent( @@ -47,11 +47,7 @@ func (c *RpcLogStreamBuilder) ListenForContractEvent( } func (c *RpcLogStreamBuilder) Build() (*RpcLogStreamer, error) { - client, err := ethclient.Dial(c.rpcUrl) - if err != nil { - return nil, err - } - return NewRpcLogStreamer(client, c.logger, c.contractConfigs), nil + return NewRpcLogStreamer(c.ethclient, c.logger, c.contractConfigs), nil } // Struct defining all the information required to filter events from logs diff --git a/pkg/indexer/blockchain/rpcLogStreamer_test.go b/pkg/indexer/blockchain/rpcLogStreamer_test.go index 7d2d24ad..e7e6629c 100644 --- a/pkg/indexer/blockchain/rpcLogStreamer_test.go +++ b/pkg/indexer/blockchain/rpcLogStreamer_test.go @@ -1,6 +1,7 @@ package blockchain import ( + "context" big "math/big" "testing" @@ -15,10 +16,6 @@ import ( "go.uber.org/zap" ) -// Using a free RPC url so that the dial function works. -// May be unwise or flaky and we may need to reconsider -const RPC_URL = "https://nodes.mewapi.io/rpc/eth" - func buildStreamer( t *testing.T, client ChainClient, @@ -39,7 +36,9 @@ func buildStreamer( } func TestBuilder(t *testing.T) { - builder := NewRpcLogStreamBuilder(RPC_URL, testutils.NewLog(t)) + testclient, err := NewClient(context.Background(), testutils.GetContractsOptions(t).RpcUrl) + require.NoError(t, err) + builder := NewRpcLogStreamBuilder(testclient, testutils.NewLog(t)) listenerChannel := builder.ListenForContractEvent( 1, diff --git a/pkg/indexer/indexer.go b/pkg/indexer/indexer.go index 718a8fac..a53908f0 100644 --- a/pkg/indexer/indexer.go +++ b/pkg/indexer/indexer.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" "github.com/xmtp/xmtpd/pkg/abis" "github.com/xmtp/xmtpd/pkg/config" "github.com/xmtp/xmtpd/pkg/db/queries" @@ -22,7 +23,11 @@ func StartIndexer( queries *queries.Queries, cfg config.ContractsOptions, ) error { - builder := blockchain.NewRpcLogStreamBuilder(cfg.RpcUrl, logger) + client, err := blockchain.NewClient(ctx, cfg.RpcUrl) + if err != nil { + return err + } + builder := blockchain.NewRpcLogStreamBuilder(client, logger) messagesTopic, err := buildMessagesTopic() if err != nil { @@ -35,11 +40,16 @@ func StartIndexer( []common.Hash{messagesTopic}, ) + messagesContract, err := messagesContract(cfg, client) + if err != nil { + return err + } + indexLogs( ctx, messagesChannel, logger.Named("indexLogs").With(zap.String("contractAddress", cfg.MessagesContractAddress)), - storer.NewGroupMessageStorer(queries, logger), + storer.NewGroupMessageStorer(queries, logger, messagesContract), ) streamer, err := builder.Build() @@ -92,3 +102,13 @@ func buildMessagesTopic() (common.Hash, error) { } return utils.GetEventTopic(abi, "MessageSent") } + +func messagesContract( + cfg config.ContractsOptions, + client *ethclient.Client, +) (*abis.GroupMessages, error) { + return abis.NewGroupMessages( + common.HexToAddress(cfg.MessagesContractAddress), + client, + ) +} diff --git a/pkg/indexer/storer/groupMessage.go b/pkg/indexer/storer/groupMessage.go index 3858ecde..4f6e1c47 100644 --- a/pkg/indexer/storer/groupMessage.go +++ b/pkg/indexer/storer/groupMessage.go @@ -2,23 +2,55 @@ package storer import ( "context" - "errors" + "fmt" "github.com/ethereum/go-ethereum/core/types" + "github.com/xmtp/xmtpd/pkg/abis" "github.com/xmtp/xmtpd/pkg/db/queries" "go.uber.org/zap" ) type GroupMessageStorer struct { - queries *queries.Queries - logger *zap.Logger + contract *abis.GroupMessages + queries *queries.Queries + logger *zap.Logger } -func NewGroupMessageStorer(queries *queries.Queries, logger *zap.Logger) *GroupMessageStorer { - return &GroupMessageStorer{queries: queries, logger: logger} +func NewGroupMessageStorer( + queries *queries.Queries, + logger *zap.Logger, + contract *abis.GroupMessages, +) *GroupMessageStorer { + return &GroupMessageStorer{queries: queries, logger: logger, contract: contract} } // Validate and store a group message log event func (s *GroupMessageStorer) StoreLog(ctx context.Context, event types.Log) LogStorageError { - return NewLogStorageError(errors.New("not implemented"), true) + msgSent, err := s.contract.ParseMessageSent(event) + if err != nil { + return NewLogStorageError(err, false) + } + + // TODO:nm figure out topic structure + topic := buildTopic(msgSent.GroupId) + + s.logger.Debug("Inserting message from contract", zap.String("topic", topic)) + + if _, err = s.queries.InsertGatewayEnvelope(ctx, queries.InsertGatewayEnvelopeParams{ + // We may not want to hardcode this to 0 and have an originator ID for each smart contract? + OriginatorID: 0, + OriginatorSequenceID: int64(msgSent.SequenceId), + Topic: []byte(topic), + OriginatorEnvelope: msgSent.Message, // TODO:nm parse originator envelope and do some validation + }); err != nil { + s.logger.Error("Error inserting envelope from smart contract", zap.Error(err)) + return NewLogStorageError(err, true) + } + + return nil +} + +func buildTopic(groupId [32]byte) string { + // We should think about simplifying the topics, since backwards compatibility shouldn't really matter here + return fmt.Sprintf("/xmtp/1/g-%x/proto", groupId) } diff --git a/pkg/indexer/storer/groupMessage_test.go b/pkg/indexer/storer/groupMessage_test.go new file mode 100644 index 00000000..416c4a9d --- /dev/null +++ b/pkg/indexer/storer/groupMessage_test.go @@ -0,0 +1,123 @@ +package storer + +import ( + "context" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" + "github.com/xmtp/xmtpd/pkg/abis" + "github.com/xmtp/xmtpd/pkg/db" + "github.com/xmtp/xmtpd/pkg/db/queries" + "github.com/xmtp/xmtpd/pkg/indexer/blockchain" + testutils "github.com/xmtp/xmtpd/pkg/testing" + "github.com/xmtp/xmtpd/pkg/utils" +) + +func buildGroupMessageStorer(t *testing.T) (*GroupMessageStorer, func()) { + ctx, cancel := context.WithCancel(context.Background()) + db, _, cleanup := testutils.NewDB(t, ctx) + queryImpl := queries.New(db) + config := testutils.GetContractsOptions(t) + contractAddress := config.MessagesContractAddress + + client, err := blockchain.NewClient(ctx, config.RpcUrl) + require.NoError(t, err) + contract, err := abis.NewGroupMessages( + common.HexToAddress(contractAddress), + client, + ) + + require.NoError(t, err) + storer := NewGroupMessageStorer(queryImpl, testutils.NewLog(t), contract) + + return storer, func() { + cancel() + cleanup() + } +} + +func TestStoreGroupMessages(t *testing.T) { + ctx := context.Background() + storer, cleanup := buildGroupMessageStorer(t) + defer cleanup() + + var groupID [32]byte + copy(groupID[:], testutils.RandomBytes(32)) + message := testutils.RandomBytes(30) + sequenceID := uint64(1) + + logMessage := testutils.BuildMessageSentLog(t, groupID, message, sequenceID) + + err := storer.StoreLog( + ctx, + logMessage, + ) + require.NoError(t, err) + + envelopes, queryErr := storer.queries.SelectGatewayEnvelopes( + ctx, + queries.SelectGatewayEnvelopesParams{OriginatorNodeID: db.NullInt32(0)}, + ) + require.NoError(t, queryErr) + + require.Equal(t, len(envelopes), 1) + + firstEnvelope := envelopes[0] + require.Equal(t, firstEnvelope.OriginatorEnvelope, message) +} + +func TestStoreGroupMessageDuplicate(t *testing.T) { + ctx := context.Background() + storer, cleanup := buildGroupMessageStorer(t) + defer cleanup() + + var groupID [32]byte + copy(groupID[:], testutils.RandomBytes(32)) + message := testutils.RandomBytes(30) + sequenceID := uint64(1) + + logMessage := testutils.BuildMessageSentLog(t, groupID, message, sequenceID) + + err := storer.StoreLog( + ctx, + logMessage, + ) + require.NoError(t, err) + // Store the log a second time + err = storer.StoreLog( + ctx, + logMessage, + ) + require.NoError(t, err) + + envelopes, queryErr := storer.queries.SelectGatewayEnvelopes( + ctx, + queries.SelectGatewayEnvelopesParams{OriginatorNodeID: db.NullInt32(0)}, + ) + require.NoError(t, queryErr) + + require.Equal(t, len(envelopes), 1) +} + +func TestStoreGroupMessageMalformed(t *testing.T) { + ctx := context.Background() + storer, cleanup := buildGroupMessageStorer(t) + defer cleanup() + + abi, err := abis.GroupMessagesMetaData.GetAbi() + require.NoError(t, err) + + topic, err := utils.GetEventTopic(abi, "MessageSent") + require.NoError(t, err) + + logMessage := types.Log{ + Topics: []common.Hash{topic}, + Data: []byte("foo"), + } + + storageErr := storer.StoreLog(ctx, logMessage) + require.Error(t, storageErr) + require.False(t, storageErr.ShouldRetry()) +} diff --git a/pkg/mocks/mock_ChainClient.go b/pkg/mocks/mock_ChainClient.go index 4232225c..3c1ead6d 100644 --- a/pkg/mocks/mock_ChainClient.go +++ b/pkg/mocks/mock_ChainClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.44.1. DO NOT EDIT. package mocks diff --git a/pkg/mocks/mock_LogStorer.go b/pkg/mocks/mock_LogStorer.go index 9e5ba37f..177b49c6 100644 --- a/pkg/mocks/mock_LogStorer.go +++ b/pkg/mocks/mock_LogStorer.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.44.1. DO NOT EDIT. package mocks diff --git a/pkg/mocks/mock_NodeRegistry.go b/pkg/mocks/mock_NodeRegistry.go index 6fd658f5..ced56dd4 100644 --- a/pkg/mocks/mock_NodeRegistry.go +++ b/pkg/mocks/mock_NodeRegistry.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.44.1. DO NOT EDIT. package mocks diff --git a/pkg/mocks/mock_NodesContract.go b/pkg/mocks/mock_NodesContract.go index 8a1532b4..483d5e6e 100644 --- a/pkg/mocks/mock_NodesContract.go +++ b/pkg/mocks/mock_NodesContract.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.44.1. DO NOT EDIT. package mocks diff --git a/pkg/proto/mls/message_contents/content.pb.go b/pkg/proto/mls/message_contents/content.pb.go index 802e2837..49130a2c 100644 --- a/pkg/proto/mls/message_contents/content.pb.go +++ b/pkg/proto/mls/message_contents/content.pb.go @@ -387,10 +387,6 @@ type MessageHistoryReply struct { Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` // Generated input 'secret' for the AES Key used to encrypt the message-bundle EncryptionKey *MessageHistoryKeyType `protobuf:"bytes,3,opt,name=encryption_key,json=encryptionKey,proto3" json:"encryption_key,omitempty"` - // Generated input 'secret' for the HMAC Key used to sign the bundle_hash - SigningKey *MessageHistoryKeyType `protobuf:"bytes,4,opt,name=signing_key,json=signingKey,proto3" json:"signing_key,omitempty"` - // HMAC Signature of the message-bundle - BundleHash []byte `protobuf:"bytes,5,opt,name=bundle_hash,json=bundleHash,proto3" json:"bundle_hash,omitempty"` } func (x *MessageHistoryReply) Reset() { @@ -446,21 +442,7 @@ func (x *MessageHistoryReply) GetEncryptionKey() *MessageHistoryKeyType { return nil } -func (x *MessageHistoryReply) GetSigningKey() *MessageHistoryKeyType { - if x != nil { - return x.SigningKey - } - return nil -} - -func (x *MessageHistoryReply) GetBundleHash() []byte { - if x != nil { - return x.BundleHash - } - return nil -} - -// Key used to encrypt or sign the message-bundle +// Key used to encrypt the message-bundle type MessageHistoryKeyType struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -772,8 +754,8 @@ var file_mls_message_contents_content_proto_rawDesc = []byte{ 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x70, 0x69, 0x6e, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x69, 0x6e, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x93, - 0x02, 0x0a, 0x13, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x69, 0x6e, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x9f, + 0x01, 0x0a, 0x13, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, @@ -783,38 +765,30 @@ var file_mls_message_contents_content_proto_rawDesc = []byte{ 0x67, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x4b, 0x65, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0d, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, - 0x12, 0x51, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x78, 0x6d, 0x74, 0x70, 0x2e, 0x6d, 0x6c, 0x73, - 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, - 0x73, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, - 0x4b, 0x65, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, - 0x4b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x5f, 0x68, 0x61, - 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, - 0x48, 0x61, 0x73, 0x68, 0x22, 0x4d, 0x0a, 0x15, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, - 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x4b, 0x65, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2d, 0x0a, - 0x11, 0x63, 0x68, 0x61, 0x63, 0x68, 0x61, 0x32, 0x30, 0x5f, 0x70, 0x6f, 0x6c, 0x79, 0x31, 0x33, - 0x30, 0x35, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x10, 0x63, 0x68, 0x61, 0x63, - 0x68, 0x61, 0x32, 0x30, 0x50, 0x6f, 0x6c, 0x79, 0x31, 0x33, 0x30, 0x35, 0x42, 0x05, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x2a, 0x3c, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x13, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x49, 0x4f, - 0x4e, 0x5f, 0x44, 0x45, 0x46, 0x4c, 0x41, 0x54, 0x45, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x43, - 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x47, 0x5a, 0x49, 0x50, 0x10, - 0x01, 0x42, 0xe5, 0x01, 0x0a, 0x1d, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x6d, 0x74, 0x70, 0x2e, 0x6d, - 0x6c, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, - 0x6e, 0x74, 0x73, 0x42, 0x0c, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x74, - 0x6f, 0x50, 0x01, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x78, 0x6d, 0x74, 0x70, 0x2f, 0x78, 0x6d, 0x74, 0x70, 0x64, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6d, 0x6c, 0x73, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0xa2, 0x02, 0x03, 0x58, 0x4d, 0x4d, 0xaa, - 0x02, 0x18, 0x58, 0x6d, 0x74, 0x70, 0x2e, 0x4d, 0x6c, 0x73, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0xca, 0x02, 0x18, 0x58, 0x6d, 0x74, - 0x70, 0x5c, 0x4d, 0x6c, 0x73, 0x5c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x43, 0x6f, 0x6e, - 0x74, 0x65, 0x6e, 0x74, 0x73, 0xe2, 0x02, 0x24, 0x58, 0x6d, 0x74, 0x70, 0x5c, 0x4d, 0x6c, 0x73, - 0x5c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, - 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1a, 0x58, - 0x6d, 0x74, 0x70, 0x3a, 0x3a, 0x4d, 0x6c, 0x73, 0x3a, 0x3a, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x22, 0x4d, 0x0a, 0x15, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x69, 0x73, 0x74, 0x6f, + 0x72, 0x79, 0x4b, 0x65, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2d, 0x0a, 0x11, 0x63, 0x68, 0x61, + 0x63, 0x68, 0x61, 0x32, 0x30, 0x5f, 0x70, 0x6f, 0x6c, 0x79, 0x31, 0x33, 0x30, 0x35, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x10, 0x63, 0x68, 0x61, 0x63, 0x68, 0x61, 0x32, 0x30, + 0x50, 0x6f, 0x6c, 0x79, 0x31, 0x33, 0x30, 0x35, 0x42, 0x05, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x2a, + 0x3c, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x17, + 0x0a, 0x13, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x44, 0x45, + 0x46, 0x4c, 0x41, 0x54, 0x45, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x4f, 0x4d, 0x50, 0x52, + 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x47, 0x5a, 0x49, 0x50, 0x10, 0x01, 0x42, 0xe5, 0x01, + 0x0a, 0x1d, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x6d, 0x74, 0x70, 0x2e, 0x6d, 0x6c, 0x73, 0x2e, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x42, + 0x0c, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, + 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x6d, 0x74, 0x70, + 0x2f, 0x78, 0x6d, 0x74, 0x70, 0x64, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x6d, 0x6c, 0x73, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x73, 0xa2, 0x02, 0x03, 0x58, 0x4d, 0x4d, 0xaa, 0x02, 0x18, 0x58, 0x6d, + 0x74, 0x70, 0x2e, 0x4d, 0x6c, 0x73, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x43, 0x6f, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0xca, 0x02, 0x18, 0x58, 0x6d, 0x74, 0x70, 0x5c, 0x4d, 0x6c, + 0x73, 0x5c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, + 0x73, 0xe2, 0x02, 0x24, 0x58, 0x6d, 0x74, 0x70, 0x5c, 0x4d, 0x6c, 0x73, 0x5c, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x5c, 0x47, 0x50, 0x42, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1a, 0x58, 0x6d, 0x74, 0x70, 0x3a, + 0x3a, 0x4d, 0x6c, 0x73, 0x3a, 0x3a, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x43, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -850,14 +824,13 @@ var file_mls_message_contents_content_proto_depIdxs = []int32{ 8, // 3: xmtp.mls.message_contents.PlaintextEnvelope.v1:type_name -> xmtp.mls.message_contents.PlaintextEnvelope.V1 9, // 4: xmtp.mls.message_contents.PlaintextEnvelope.v2:type_name -> xmtp.mls.message_contents.PlaintextEnvelope.V2 6, // 5: xmtp.mls.message_contents.MessageHistoryReply.encryption_key:type_name -> xmtp.mls.message_contents.MessageHistoryKeyType - 6, // 6: xmtp.mls.message_contents.MessageHistoryReply.signing_key:type_name -> xmtp.mls.message_contents.MessageHistoryKeyType - 4, // 7: xmtp.mls.message_contents.PlaintextEnvelope.V2.request:type_name -> xmtp.mls.message_contents.MessageHistoryRequest - 5, // 8: xmtp.mls.message_contents.PlaintextEnvelope.V2.reply:type_name -> xmtp.mls.message_contents.MessageHistoryReply - 9, // [9:9] is the sub-list for method output_type - 9, // [9:9] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name + 4, // 6: xmtp.mls.message_contents.PlaintextEnvelope.V2.request:type_name -> xmtp.mls.message_contents.MessageHistoryRequest + 5, // 7: xmtp.mls.message_contents.PlaintextEnvelope.V2.reply:type_name -> xmtp.mls.message_contents.MessageHistoryReply + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_mls_message_contents_content_proto_init() } diff --git a/pkg/testing/config.go b/pkg/testing/config.go new file mode 100644 index 00000000..021fe3f1 --- /dev/null +++ b/pkg/testing/config.go @@ -0,0 +1,73 @@ +package testing + +import ( + "encoding/json" + "os" + "path" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/xmtp/xmtpd/pkg/config" +) + +const BLOCKCHAIN_RPC_URL = "http://localhost:7545" + +type contractInfo struct { + DeployedTo string `json:"deployedTo"` +} + +/* +* +In tests it's weirdly difficult to get the working directory of the project root. + +Keep moving up the folder hierarchy until you find a go.mod +* +*/ +func rootPath(t *testing.T) string { + dir, err := os.Getwd() + require.NoError(t, err) + + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir { // reached the root directory + t.Fatal("Could not find the root directory") + } + dir = parent + } +} + +/* +* +Parse the JSON file at this location to +* +*/ +func getDeployedTo(t *testing.T, fileName string) string { + data, err := os.ReadFile(fileName) + if err != nil { + t.Fatalf("Failed to read GroupMessages.json: %v", err) + } + + var info contractInfo + + if err := json.Unmarshal(data, &info); err != nil { + t.Fatalf("Failed to parse GroupMessages.json: %v", err) + } + + return info.DeployedTo +} + +func GetContractsOptions(t *testing.T) config.ContractsOptions { + rootDir := rootPath(t) + + return config.ContractsOptions{ + RpcUrl: BLOCKCHAIN_RPC_URL, + MessagesContractAddress: getDeployedTo(t, path.Join(rootDir, "./build/GroupMessages.json")), + NodesContractAddress: getDeployedTo(t, path.Join(rootDir, "./build/Nodes.json")), + RefreshInterval: 100 * time.Millisecond, + } +} diff --git a/pkg/testing/contracts.go b/pkg/testing/contracts.go new file mode 100644 index 00000000..783ea366 --- /dev/null +++ b/pkg/testing/contracts.go @@ -0,0 +1,46 @@ +package testing + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" + "github.com/xmtp/xmtpd/pkg/abis" + "github.com/xmtp/xmtpd/pkg/utils" +) + +// Build an abi encoded MessageSent event struct +func BuildMessageSentEvent( + groupID [32]byte, + message []byte, + sequenceID uint64, +) ([]byte, error) { + abi, err := abis.GroupMessagesMetaData.GetAbi() + if err != nil { + return nil, err + } + return abi.Events["MessageSent"].Inputs.Pack(groupID, message, sequenceID) +} + +// Build a log message for a MessageSent event +func BuildMessageSentLog( + t *testing.T, + groupID [32]byte, + message []byte, + sequenceID uint64, +) types.Log { + eventData, err := BuildMessageSentEvent(groupID, message, sequenceID) + require.NoError(t, err) + + abi, err := abis.GroupMessagesMetaData.GetAbi() + require.NoError(t, err) + + topic, err := utils.GetEventTopic(abi, "MessageSent") + require.NoError(t, err) + + return types.Log{ + Topics: []common.Hash{topic}, + Data: eventData, + } +}