diff --git a/.github/workflows/abi_bindings_checker.yml b/.github/workflows/abi_bindings_checker.yml index 6d5c89f6c..6da308d99 100644 --- a/.github/workflows/abi_bindings_checker.yml +++ b/.github/workflows/abi_bindings_checker.yml @@ -44,29 +44,3 @@ jobs: - name: Fail if diff exists run: git --no-pager diff --quiet -- abi-bindings/**.go - - unit_tests: - name: Unit tests - runs-on: ubuntu-20.04 - - steps: - - name: Checkout repositories and submodules - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Set Go version - run: | - source ./scripts/versions.sh - GO_VERSION=$GO_VERSION >> $GITHUB_ENV - - - name: Setup Go - uses: actions/setup-go@v4 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Run ABI Binding Unit Tests - run: | - source scripts/constants.sh - cd abi-bindings/go - go test ./... diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3038c2999..2787151f4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: E2E and Solidity Unit Tests +name: Tests on: push: @@ -26,7 +26,31 @@ jobs: export PATH=$PATH:$HOME/.foundry/bin cd contracts/ forge test -vvv - + + go-unit-tests: + runs-on: ubuntu-20.04 + + steps: + - name: Checkout repositories and submodules + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set Go version + run: | + source ./scripts/versions.sh + GO_VERSION=$GO_VERSION >> $GITHUB_ENV + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Run Go unit tests + run: | + source scripts/constants.sh + go test ./... + e2e_tests: name: e2e_tests runs-on: ubuntu-20.04 diff --git a/.gitignore b/.gitignore index be9217d50..4aa756357 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ Cargo.lock # IDE configurations .vscode* +# Binaries +cmd/teleporter-cli/teleporter-cli + # File generated by tests relayerConfig.json subnetGenesis_* diff --git a/abi-bindings/go/Teleporter/TeleporterMessenger/event.go b/abi-bindings/go/Teleporter/TeleporterMessenger/event.go new file mode 100644 index 000000000..587dcf888 --- /dev/null +++ b/abi-bindings/go/Teleporter/TeleporterMessenger/event.go @@ -0,0 +1,101 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package teleportermessenger + +import ( + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/common" +) + +// Event is a Teleporter log event +type Event uint8 + +const ( + Unknown Event = iota + SendCrossChainMessage + ReceiveCrossChainMessage + AddFeeAmount + MessageExecutionFailed + MessageExecuted + RelayerRewardsRedeemed + + sendCrossChainMessageStr = "SendCrossChainMessage" + receiveCrossChainMessageStr = "ReceiveCrossChainMessage" + addFeeAmountStr = "AddFeeAmount" + messageExecutionFailedStr = "MessageExecutionFailed" + messageExecutedStr = "MessageExecuted" + relayerRewardsRedeemedStr = "RelayerRewardsRedeemed" + unknownStr = "Unknown" +) + +// String returns the string representation of an Event +func (e Event) String() string { + switch e { + case SendCrossChainMessage: + return sendCrossChainMessageStr + case ReceiveCrossChainMessage: + return receiveCrossChainMessageStr + case AddFeeAmount: + return addFeeAmountStr + case MessageExecutionFailed: + return messageExecutionFailedStr + case MessageExecuted: + return messageExecutedStr + case RelayerRewardsRedeemed: + return relayerRewardsRedeemedStr + default: + return unknownStr + } +} + +// ToEvent converts a string to an Event +func ToEvent(e string) (Event, error) { + switch strings.ToLower(e) { + case strings.ToLower(sendCrossChainMessageStr): + return SendCrossChainMessage, nil + case strings.ToLower(receiveCrossChainMessageStr): + return ReceiveCrossChainMessage, nil + case strings.ToLower(addFeeAmountStr): + return AddFeeAmount, nil + case strings.ToLower(messageExecutionFailedStr): + return MessageExecutionFailed, nil + case strings.ToLower(messageExecutedStr): + return MessageExecuted, nil + case strings.ToLower(relayerRewardsRedeemedStr): + return RelayerRewardsRedeemed, nil + default: + return Unknown, fmt.Errorf("unknown event %s", e) + } +} + +// FilterTeleporterEvents parses the topics and data of a Teleporter log into the corresponding Teleporter event +func FilterTeleporterEvents(topics []common.Hash, data []byte, event string) (interface{}, error) { + e, err := ToEvent(event) + if err != nil { + return nil, err + } + var out interface{} + switch e { + case SendCrossChainMessage: + out = new(TeleporterMessengerSendCrossChainMessage) + case ReceiveCrossChainMessage: + out = new(TeleporterMessengerReceiveCrossChainMessage) + case AddFeeAmount: + out = new(TeleporterMessengerAddFeeAmount) + case MessageExecutionFailed: + out = new(TeleporterMessengerMessageExecutionFailed) + case MessageExecuted: + out = new(TeleporterMessengerMessageExecuted) + case RelayerRewardsRedeemed: + out = new(TeleporterMessengerRelayerRewardsRedeemed) + default: + return nil, fmt.Errorf("unknown event %s", e.String()) + } + if err := UnpackEvent(out, e.String(), topics, data); err != nil { + return nil, err + } + return out, nil +} diff --git a/abi-bindings/go/Teleporter/TeleporterMessenger/event_test.go b/abi-bindings/go/Teleporter/TeleporterMessenger/event_test.go new file mode 100644 index 000000000..9f458a123 --- /dev/null +++ b/abi-bindings/go/Teleporter/TeleporterMessenger/event_test.go @@ -0,0 +1,143 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package teleportermessenger + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestEventString(t *testing.T) { + var ( + tests = []struct { + event Event + str string + }{ + {Unknown, unknownStr}, + {SendCrossChainMessage, sendCrossChainMessageStr}, + {ReceiveCrossChainMessage, receiveCrossChainMessageStr}, + {AddFeeAmount, addFeeAmountStr}, + {MessageExecutionFailed, messageExecutionFailedStr}, + {MessageExecuted, messageExecutedStr}, + {RelayerRewardsRedeemed, relayerRewardsRedeemedStr}, + } + ) + + for _, test := range tests { + t.Run(test.event.String(), func(t *testing.T) { + require.Equal(t, test.event.String(), test.str) + }) + } +} + +func TestToEvent(t *testing.T) { + var ( + tests = []struct { + str string + event Event + isError bool + }{ + {unknownStr, Unknown, true}, + {sendCrossChainMessageStr, SendCrossChainMessage, false}, + {receiveCrossChainMessageStr, ReceiveCrossChainMessage, false}, + {addFeeAmountStr, AddFeeAmount, false}, + {messageExecutionFailedStr, MessageExecutionFailed, false}, + {messageExecutedStr, MessageExecuted, false}, + {relayerRewardsRedeemedStr, RelayerRewardsRedeemed, false}, + } + ) + + for _, test := range tests { + t.Run(test.str, func(t *testing.T) { + event, err := ToEvent(test.str) + if test.isError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.Equal(t, test.event, event) + }) + } +} + +func TestFilterTeleporterEvents(t *testing.T) { + mockBlockchainID := [32]byte{1, 2, 3, 4} + messageID := big.NewInt(1) + message := createTestTeleporterMessage(messageID.Int64()) + feeInfo := TeleporterFeeInfo{ + FeeTokenAddress: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), + Amount: big.NewInt(1), + } + deliverer := common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567") + + teleporterABI, err := TeleporterMessengerMetaData.GetAbi() + require.NoError(t, err) + + var ( + tests = []struct { + event Event + args []interface{} + expected interface{} + }{ + { + event: SendCrossChainMessage, + args: []interface{}{ + mockBlockchainID, + messageID, + message, + feeInfo, + }, + expected: &TeleporterMessengerSendCrossChainMessage{ + DestinationBlockchainID: mockBlockchainID, + MessageID: messageID, + Message: message, + FeeInfo: feeInfo, + }, + }, + { + event: ReceiveCrossChainMessage, + args: []interface{}{ + mockBlockchainID, + messageID, + deliverer, + deliverer, + message, + }, + expected: &TeleporterMessengerReceiveCrossChainMessage{ + OriginBlockchainID: mockBlockchainID, + MessageID: messageID, + Deliverer: deliverer, + RewardRedeemer: deliverer, + Message: message, + }, + }, + { + event: MessageExecuted, + args: []interface{}{ + mockBlockchainID, + messageID, + }, + expected: &TeleporterMessengerMessageExecuted{ + OriginBlockchainID: mockBlockchainID, + MessageID: messageID, + }, + }, + } + ) + + for _, test := range tests { + t.Run(test.event.String(), func(t *testing.T) { + topics, data, err := teleporterABI.PackEvent(test.event.String(), test.args...) + require.NoError(t, err) + + out, err := FilterTeleporterEvents(topics, data, test.event.String()) + require.NoError(t, err) + + require.Equal(t, test.expected, out) + }) + } +} diff --git a/abi-bindings/go/Teleporter/TeleporterMessenger/packing.go b/abi-bindings/go/Teleporter/TeleporterMessenger/packing.go index 1bee909b1..bada9b6e5 100644 --- a/abi-bindings/go/Teleporter/TeleporterMessenger/packing.go +++ b/abi-bindings/go/Teleporter/TeleporterMessenger/packing.go @@ -124,3 +124,24 @@ func PackMessageReceivedOutput(success bool) ([]byte, error) { return abi.PackOutput("messageReceived", success) } + +// UnpackEvent unpacks the event data and topics into the provided interface +func UnpackEvent(out interface{}, event string, topics []common.Hash, data []byte) error { + teleporterABI, err := TeleporterMessengerMetaData.GetAbi() + if err != nil { + return fmt.Errorf("failed to get abi: %v", err) + } + if len(data) > 0 { + if err := teleporterABI.UnpackIntoInterface(out, event, data); err != nil { + return err + } + } + + var indexed abi.Arguments + for _, arg := range teleporterABI.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + return abi.ParseTopics(out, indexed, topics[1:]) +} diff --git a/abi-bindings/go/Teleporter/TeleporterMessenger/packing_test.go b/abi-bindings/go/Teleporter/TeleporterMessenger/packing_test.go index a586c4c07..831651814 100644 --- a/abi-bindings/go/Teleporter/TeleporterMessenger/packing_test.go +++ b/abi-bindings/go/Teleporter/TeleporterMessenger/packing_test.go @@ -14,11 +14,11 @@ import ( func createTestTeleporterMessage(messageID int64) TeleporterMessage { m := TeleporterMessage{ - MessageID: big.NewInt(messageID), - SenderAddress: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), + MessageID: big.NewInt(messageID), + SenderAddress: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), DestinationBlockchainID: [32]byte{1, 2, 3, 4}, - DestinationAddress: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), - RequiredGasLimit: big.NewInt(2), + DestinationAddress: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), + RequiredGasLimit: big.NewInt(2), AllowedRelayerAddresses: []common.Address{ common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), }, @@ -62,3 +62,85 @@ func TestPackUnpackTeleporterMessage(t *testing.T) { require.True(t, bytes.Equal(message.Message, unpacked.Message)) } + +func TestUnpackEvent(t *testing.T) { + mockBlockchainID := [32]byte{1, 2, 3, 4} + messageID := big.NewInt(1) + message := createTestTeleporterMessage(messageID.Int64()) + feeInfo := TeleporterFeeInfo{ + FeeTokenAddress: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), + Amount: big.NewInt(1), + } + deliverer := common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567") + + teleporterABI, err := TeleporterMessengerMetaData.GetAbi() + require.NoError(t, err) + + var ( + tests = []struct { + event Event + args []interface{} + out interface{} + expected interface{} + }{ + { + event: SendCrossChainMessage, + args: []interface{}{ + mockBlockchainID, + messageID, + message, + feeInfo, + }, + out: new(TeleporterMessengerSendCrossChainMessage), + expected: &TeleporterMessengerSendCrossChainMessage{ + DestinationBlockchainID: mockBlockchainID, + MessageID: messageID, + Message: message, + FeeInfo: feeInfo, + }, + }, + { + event: ReceiveCrossChainMessage, + args: []interface{}{ + mockBlockchainID, + messageID, + deliverer, + deliverer, + message, + }, + out: new(TeleporterMessengerReceiveCrossChainMessage), + expected: &TeleporterMessengerReceiveCrossChainMessage{ + OriginBlockchainID: mockBlockchainID, + MessageID: messageID, + Deliverer: deliverer, + RewardRedeemer: deliverer, + Message: message, + }, + }, + { + event: MessageExecuted, + args: []interface{}{ + mockBlockchainID, + messageID, + }, + out: new(TeleporterMessengerMessageExecuted), + expected: &TeleporterMessengerMessageExecuted{ + OriginBlockchainID: mockBlockchainID, + MessageID: messageID, + }, + }, + } + ) + + for _, test := range tests { + t.Run(test.event.String(), func(t *testing.T) { + topics, data, err := teleporterABI.PackEvent(test.event.String(), test.args...) + require.NoError(t, err) + + err = UnpackEvent(test.out, test.event.String(), topics, data) + require.NoError(t, err) + + require.Equal(t, test.expected, test.out) + }) + } +} diff --git a/cmd/teleporter-cli/README.md b/cmd/teleporter-cli/README.md new file mode 100644 index 000000000..fe912c114 --- /dev/null +++ b/cmd/teleporter-cli/README.md @@ -0,0 +1,17 @@ +# Teleporter CLI + +This directory contains the source code for the Teleporter CLI. The CLI is a command line interface for interacting with the Teleporter contracts. It is written with [cobra](https://github.com/spf13/cobra) commands as a Go application. + +## Build + +To build the CLI, run `go build` from this directory. This will create a binary called `teleporter-cli` in the current directory. + +## Usage + +The CLI has a number of subcommands. To see the list of subcommands, run `./teleporter-cli help`. To see the help for a specific subcommand, run `./teleporter-cli help `. + +The supported subcommands include: + +- `event`: given a log event's topics and data, attempts to decode into a Teleporter event in a more readable format. +- `message`: given a Teleporter message encoded as a hex string, attempts to decode into a Teleporter message in a more readable format. +- `transaction`: given a transaction hash, attempts to decode all relevant Teleporter and Warp log events in a more readable format. diff --git a/cmd/teleporter-cli/event.go b/cmd/teleporter-cli/event.go new file mode 100644 index 000000000..0dd50eb28 --- /dev/null +++ b/cmd/teleporter-cli/event.go @@ -0,0 +1,50 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + teleportermessenger "github.com/ava-labs/teleporter/abi-bindings/go/Teleporter/TeleporterMessenger" + "github.com/ethereum/go-ethereum/common" + "github.com/spf13/cobra" + "go.uber.org/zap" +) + +var ( + topicArgs []string + data []byte +) + +var eventCmd = &cobra.Command{ + Use: "event --topics topic1,topic2 [--data data]", + Short: "Parses a Teleporter log's topics and data", + Long: `Given the topics and data of a Teleporter log, parses the log into +the corresponding Teleporter event. Topics are represented by a hash, +and data is the hex encoding of the bytes.`, + Args: cobra.NoArgs, + Run: eventRun, +} + +func eventRun(cmd *cobra.Command, args []string) { + var topics []common.Hash + for _, topic := range topicArgs { + topics = append(topics, common.HexToHash(topic)) + } + + event, err := teleporterABI.EventByID(topics[0]) + cobra.CheckErr(err) + + out, err := teleportermessenger.FilterTeleporterEvents(topics, data, event.Name) + cobra.CheckErr(err) + logger.Info("Parsed Teleporter event", zap.String("name", event.Name), zap.Any("event", out)) + cmd.Println("Event command ran successfully for", event.Name) +} + +func init() { + rootCmd.AddCommand(eventCmd) + eventCmd.PersistentFlags().StringSliceVar(&topicArgs, "topics", []string{}, "Topic hashes of the event") + eventCmd.Flags().BytesHexVar(&data, "data", []byte{}, "Hex encoded data of the event") + + err := eventCmd.MarkPersistentFlagRequired("topics") + cobra.CheckErr(err) +} diff --git a/cmd/teleporter-cli/event_test.go b/cmd/teleporter-cli/event_test.go new file mode 100644 index 000000000..7cdc808d8 --- /dev/null +++ b/cmd/teleporter-cli/event_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEventCmd(t *testing.T) { + var tests = []struct { + name string + args []string + err error + out string + }{ + { + name: "no args", + args: []string{"event"}, + err: fmt.Errorf("required flag(s) \"topics\" not set"), + }, + { + name: "help", + args: []string{"event", "--help"}, + err: nil, + out: "Given the topics and data of a Teleporter log", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out, err := executeTestCmd(t, rootCmd, tt.args...) + if tt.err != nil { + require.ErrorContains(t, err, tt.err.Error()) + } else { + require.NoError(t, err) + require.Contains(t, out, tt.out) + } + }) + } +} diff --git a/cmd/teleporter-cli/message.go b/cmd/teleporter-cli/message.go new file mode 100644 index 000000000..aaff6d441 --- /dev/null +++ b/cmd/teleporter-cli/message.go @@ -0,0 +1,34 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "encoding/hex" + + teleportermessenger "github.com/ava-labs/teleporter/abi-bindings/go/Teleporter/TeleporterMessenger" + "github.com/spf13/cobra" + "go.uber.org/zap" +) + +var messageCmd = &cobra.Command{ + Use: "message MESSAGE_BYTES", + Short: "Decodes hex encoded Teleporter message bytes into a TeleporterMessage struct", + Long: `Given the hex encoded bytes of a Teleporter message, this command will decode +the bytes into a TeleporterMessage struct and print the struct fields.`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + encodedMsg := args[0] + b, err := hex.DecodeString(encodedMsg) + cobra.CheckErr(err) + + msg, err := teleportermessenger.UnpackTeleporterMessage(b) + cobra.CheckErr(err) + logger.Info("Teleporter Message unpacked", zap.Any("message", msg)) + cmd.Println("Message command ran successfully") + }, +} + +func init() { + rootCmd.AddCommand(messageCmd) +} diff --git a/cmd/teleporter-cli/message_test.go b/cmd/teleporter-cli/message_test.go new file mode 100644 index 000000000..59443a2e6 --- /dev/null +++ b/cmd/teleporter-cli/message_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMessageCmd(t *testing.T) { + var tests = []struct { + name string + args []string + err error + out string + }{ + { + name: "no args", + args: []string{"message"}, + err: fmt.Errorf("accepts 1 arg(s), received 0"), + }, + { + name: "help", + args: []string{"message", "--help"}, + err: nil, + out: "Given the hex encoded bytes of a Teleporter message", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out, err := executeTestCmd(t, rootCmd, tt.args...) + if tt.err != nil { + require.ErrorContains(t, err, tt.err.Error()) + } else { + require.NoError(t, err) + require.Contains(t, out, tt.out) + } + }) + } +} diff --git a/cmd/teleporter-cli/root.go b/cmd/teleporter-cli/root.go new file mode 100644 index 000000000..f6a4b6141 --- /dev/null +++ b/cmd/teleporter-cli/root.go @@ -0,0 +1,81 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "os" + + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/subnet-evm/accounts/abi" + teleportermessenger "github.com/ava-labs/teleporter/abi-bindings/go/Teleporter/TeleporterMessenger" + "github.com/spf13/cobra" +) + +var ( + logger logging.Logger + teleporterABI *abi.ABI +) + +var rootCmd = &cobra.Command{ + Use: "teleporter-cli", + Short: "A CLI that integrates with the Teleporter protocol", + Long: `A CLI that integrates with the Teleporter protocol, and allows you +to debug Teleporter on chain activity. The CLI can help decode +Teleporter and Warp events, as well as parsing Teleporter messages.`, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.CompletionOptions.DisableDefaultCmd = true + logLevelArg := rootCmd.PersistentFlags().StringP("log", "l", "", "Log level i.e. debug, info...") + rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + return rootPreRunE(logLevelArg) + } +} + +func rootPreRunE(logLevelArg *string) error { + if *logLevelArg == "" { + *logLevelArg = logging.Info.LowerString() + } + + logLevel, err := logging.ToLevel(*logLevelArg) + if err != nil { + return err + } + logger = logging.NewLogger( + "teleporter-cli", + logging.NewWrappedCore( + logLevel, + os.Stdout, + logging.Plain.ConsoleEncoder(), + ), + ) + abi, err := teleportermessenger.TeleporterMessengerMetaData.GetAbi() + if err != nil { + return err + } + teleporterABI = abi + return nil +} + +func callPersistentPreRunE(cmd *cobra.Command, args []string) error { + if parent := cmd.Parent(); parent != nil { + if parent.PersistentPreRunE != nil { + return parent.PersistentPreRunE(parent, args) + } + } + return nil +} + +func main() { + Execute() +} diff --git a/cmd/teleporter-cli/root_test.go b/cmd/teleporter-cli/root_test.go new file mode 100644 index 000000000..d9cbac65f --- /dev/null +++ b/cmd/teleporter-cli/root_test.go @@ -0,0 +1,60 @@ +package main + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +func executeTestCmd(t *testing.T, c *cobra.Command, args ...string) (string, error) { + buf := new(bytes.Buffer) + c.SetOut(buf) + c.SetErr(buf) + c.SetArgs(args) + + err := c.Execute() + return strings.TrimSpace(buf.String()), err +} + +func TestRootCmd(t *testing.T) { + var tests = []struct { + name string + args []string + err error + out string + }{ + { + name: "base", + args: []string{}, + err: nil, + out: "A CLI that integrates with the Teleporter protocol", + }, + { + name: "help", + args: []string{"--help"}, + err: nil, + out: "A CLI that integrates with the Teleporter protocol", + }, + { + name: "invalid", + args: []string{"invalid"}, + err: fmt.Errorf("unknown command"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out, err := executeTestCmd(t, rootCmd, tt.args...) + if tt.err != nil { + require.ErrorContains(t, err, tt.err.Error()) + } else { + require.NoError(t, err) + require.Contains(t, out, tt.out) + } + }) + } +} diff --git a/cmd/teleporter-cli/transaction.go b/cmd/teleporter-cli/transaction.go new file mode 100644 index 000000000..29e12f369 --- /dev/null +++ b/cmd/teleporter-cli/transaction.go @@ -0,0 +1,99 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + + warpPayload "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/subnet-evm/ethclient" + "github.com/ava-labs/subnet-evm/x/warp" + teleportermessenger "github.com/ava-labs/teleporter/abi-bindings/go/Teleporter/TeleporterMessenger" + "github.com/ethereum/go-ethereum/common" + "github.com/spf13/cobra" + "go.uber.org/zap" +) + +const ( + warpPrecompileAddress = "0x0200000000000000000000000000000000000005" +) + +var ( + rpcEndpoint string + teleporterAddress common.Address + client ethclient.Client +) + +var transactionCmd = &cobra.Command{ + Use: "transaction --rpc RPC_URL --teleporter-address CONTRACT_ADDRESS TRANSACTION_HASH", + Short: "Parses relevant Teleporter logs from a transaction", + Long: `Given a transaction this command looks through the transaction's receipt +for Teleporter and Warp log events. When corresponding log events are found, +the command parses to log event fields to a more human readable format.`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + receipt, err := client.TransactionReceipt(context.Background(), + common.HexToHash(args[0])) + cobra.CheckErr(err) + + for _, log := range receipt.Logs { + if log.Address == teleporterAddress { + logger.Info("Processing Teleporter log", zap.Any("log", log)) + + event, err := teleporterABI.EventByID(log.Topics[0]) + cobra.CheckErr(err) + + out, err := teleportermessenger.FilterTeleporterEvents(log.Topics, log.Data, event.Name) + cobra.CheckErr(err) + logger.Info("Parsed Teleporter event", zap.String("name", event.Name), zap.Any("event", out)) + } + + if log.Address == common.HexToAddress(warpPrecompileAddress) { + logger.Debug("Processing Warp log", zap.Any("log", log)) + + unsignedMsg, err := warp.UnpackSendWarpEventDataToMessage(log.Data) + cobra.CheckErr(err) + + warpPayload, err := warpPayload.ParseAddressedCall(unsignedMsg.Payload) + cobra.CheckErr(err) + + teleporterMessage, err := teleportermessenger.UnpackTeleporterMessage(warpPayload.Payload) + cobra.CheckErr(err) + logger.Info("Parsed Teleporter message", + zap.String("warpMessageID", unsignedMsg.ID().Hex()), + zap.String("teleporterMessageID", teleporterMessage.MessageID.String()), + zap.Any("message", teleporterMessage)) + } + } + cmd.Println("Transaction command ran successfully") + }, +} + +func init() { + rootCmd.AddCommand(transactionCmd) + transactionCmd.PersistentFlags().StringVar(&rpcEndpoint, "rpc", "", "RPC endpoint to connect to the node") + address := transactionCmd.PersistentFlags().StringP("teleporter-address", "t", "", "Teleporter contract address") + err := transactionCmd.MarkPersistentFlagRequired("rpc") + cobra.CheckErr(err) + err = transactionCmd.MarkPersistentFlagRequired("teleporter-address") + cobra.CheckErr(err) + transactionCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + return transactionPreRunE(cmd, args, address) + } +} + +func transactionPreRunE(cmd *cobra.Command, args []string, address *string) error { + // Run the persistent pre-run function of the root command if it exists. + if err := callPersistentPreRunE(cmd, args); err != nil { + return err + } + teleporterAddress = common.HexToAddress(*address) + c, err := ethclient.Dial(rpcEndpoint) + if err != nil { + return err + } + + client = c + return err +} diff --git a/cmd/teleporter-cli/transaction_test.go b/cmd/teleporter-cli/transaction_test.go new file mode 100644 index 000000000..ad1371ac7 --- /dev/null +++ b/cmd/teleporter-cli/transaction_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTransactionCmd(t *testing.T) { + var tests = []struct { + name string + args []string + err error + out string + }{ + { + name: "no args", + args: []string{"transaction"}, + err: fmt.Errorf("accepts 1 arg(s), received 0"), + }, + { + name: "help", + args: []string{"transaction", "--help"}, + err: nil, + out: "Given a transaction this command looks through the transaction's receipt", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out, err := executeTestCmd(t, rootCmd, tt.args...) + if tt.err != nil { + require.ErrorContains(t, err, tt.err.Error()) + } else { + require.NoError(t, err) + require.Contains(t, out, tt.out) + } + }) + } +} diff --git a/go.mod b/go.mod index 9eac164dd..2c136e1d0 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,9 @@ require ( github.com/onsi/ginkgo/v2 v2.13.2 github.com/onsi/gomega v1.30.0 github.com/pkg/errors v0.9.1 + github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.4 + go.uber.org/zap v1.26.0 ) require ( @@ -71,6 +73,7 @@ require ( github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.2.3 // indirect github.com/huin/goupnp v1.0.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackpal/gateway v1.0.6 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/klauspost/compress v1.15.15 // indirect @@ -123,7 +126,6 @@ require ( go.opentelemetry.io/proto/otlp v0.19.0 // indirect go.uber.org/mock v0.2.0 // 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/exp v0.0.0-20230626212559-97b1e661b5df // indirect golang.org/x/mod v0.13.0 // indirect diff --git a/go.sum b/go.sum index 9c56d2df6..20ce9bd49 100644 --- a/go.sum +++ b/go.sum @@ -346,6 +346,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= @@ -515,6 +517,8 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU 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/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= diff --git a/scripts/lint.sh b/scripts/lint.sh index dc9941c5b..fb379da33 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -28,7 +28,7 @@ fi function sollinter() { # lint solidity contracts - echo "Linting Teleporter Solidity contracts..." + echo "Linting Solidity contracts..." cd $TELEPORTER_PATH/contracts/src # "solhint **/*.sol" runs differently than "solhint '**/*.sol'", where the latter checks sol files # in subdirectories. The former only checks sol files in the current directory and directories one level down. @@ -39,8 +39,8 @@ function golanglinter() { # lint e2e tests go code go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION} - echo "Linting Teleporter E2E tests Golang code..." - cd $TELEPORTER_PATH/tests + echo "Linting Golang code..." + cd $TELEPORTER_PATH/ golangci-lint run --config=$TELEPORTER_PATH/.golangci.yml ./... } diff --git a/utils/contract-deployment/contractDeploymentTools.go b/utils/contract-deployment/contractDeploymentTools.go index bf7fbe26e..59064b242 100644 --- a/utils/contract-deployment/contractDeploymentTools.go +++ b/utils/contract-deployment/contractDeploymentTools.go @@ -49,5 +49,4 @@ func main() { default: log.Panic("Invalid command type. Supported options are \"constructKeylessTx\" and \"deriveContractAddress\".") } - } diff --git a/utils/deployment-utils/deployment_utils.go b/utils/deployment-utils/deployment_utils.go index 734a55df8..0263c030d 100644 --- a/utils/deployment-utils/deployment_utils.go +++ b/utils/deployment-utils/deployment_utils.go @@ -86,7 +86,8 @@ func ExtractByteCode(byteCodeFileName string) ([]byte, error) { // Constructs a keyless transaction using Nick's method // Optionally writes the transaction, deployer address, and contract address to file // Returns the transaction bytes, deployer address, and contract address -func ConstructKeylessTransaction(byteCodeFileName string, writeFile bool) ([]byte, common.Address, common.Address, error) { +func ConstructKeylessTransaction(byteCodeFileName string, + writeFile bool) ([]byte, common.Address, common.Address, error) { // Convert the R and S values (which must be the same) from hex. rsValue, ok := new(big.Int).SetString(rsValueHex, 16) if !ok { @@ -114,7 +115,7 @@ func ConstructKeylessTransaction(byteCodeFileName string, writeFile bool) ([]byt // Recover the "sender" address of the transaction. senderAddress, err := types.HomesteadSigner{}.Sender(contractCreationTx) if err != nil { - return nil, common.Address{}, common.Address{}, errors.Wrap(err, "Failed to recover the sender address of transaction") + return nil, common.Address{}, common.Address{}, errors.Wrap(err, "Failed to recover the transaction sender address") } // Serialize the raw transaction and sender address.