From ec12868570fa996b959249a247643e88b91bcaf1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 04:04:44 +0000 Subject: [PATCH 01/11] Improve error handling for HTTP status code 0 errors Co-Authored-By: Chris Li --- core/taskengine/engine.go | 2 +- core/taskengine/executor.go | 2 +- core/taskengine/vm_runner_rest.go | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/taskengine/engine.go b/core/taskengine/engine.go index cc104476..a3bddcd7 100644 --- a/core/taskengine/engine.go +++ b/core/taskengine/engine.go @@ -603,7 +603,7 @@ func (n *Engine) TriggerTask(user *model.User, payload *avsproto.UserTriggerTask }, nil } - return nil, grpcstatus.Errorf(codes.Code(avsproto.Error_TaskTriggerError), fmt.Sprintf("error trigger task: %s", err.Error())) + return nil, grpcstatus.Errorf(codes.Code(avsproto.Error_TaskTriggerError), "error trigger task: %v", err) } data, err := json.Marshal(queueTaskData) diff --git a/core/taskengine/executor.go b/core/taskengine/executor.go index 5a2509cf..c3795fbb 100644 --- a/core/taskengine/executor.go +++ b/core/taskengine/executor.go @@ -171,5 +171,5 @@ func (x *TaskExecutor) RunTask(task *model.Task, queueData *QueueExecutionData) x.logger.Info("succesfully executing task", "task_id", task.Id, "triggermark", triggerMetadata) return execution, nil } - return execution, fmt.Errorf("Error executing task %s %v", task.Id, runTaskErr) + return execution, fmt.Errorf("Error executing task %s: %v", task.Id, runTaskErr) } diff --git a/core/taskengine/vm_runner_rest.go b/core/taskengine/vm_runner_rest.go index 4e259d1d..459ca5c3 100644 --- a/core/taskengine/vm_runner_rest.go +++ b/core/taskengine/vm_runner_rest.go @@ -149,8 +149,12 @@ func (r *RestProcessor) Execute(stepID string, node *avsproto.RestAPINode) (*avs // Check if the response status code is not 2xx or 3xx, we consider it as an error exeuction if resp.StatusCode() < 200 || resp.StatusCode() >= 400 { s.Success = false - s.Error = fmt.Sprintf("unexpected status code: %d", resp.StatusCode()) - return s, fmt.Errorf("unexpected status code: %d", resp.StatusCode()) + errorMsg := fmt.Sprintf("unexpected status code: %d", resp.StatusCode()) + if resp.StatusCode() == 0 { + errorMsg = "HTTP request failed: connection error, timeout, or DNS resolution failure (status code: 0)" + } + s.Error = errorMsg + return s, fmt.Errorf(errorMsg) } } From 0e65e29377366bb60467ab00024e93f8771ce28b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 04:11:29 +0000 Subject: [PATCH 02/11] Add unit test for HTTP error handling and fix error message capitalization Co-Authored-By: Chris Li --- core/taskengine/engine.go | 2 +- core/taskengine/vm_runner_rest_test.go | 79 ++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/core/taskengine/engine.go b/core/taskengine/engine.go index a3bddcd7..a3fec6e2 100644 --- a/core/taskengine/engine.go +++ b/core/taskengine/engine.go @@ -603,7 +603,7 @@ func (n *Engine) TriggerTask(user *model.User, payload *avsproto.UserTriggerTask }, nil } - return nil, grpcstatus.Errorf(codes.Code(avsproto.Error_TaskTriggerError), "error trigger task: %v", err) + return nil, grpcstatus.Errorf(codes.Code(avsproto.Error_TaskTriggerError), "Error trigger task: %v", err) } data, err := json.Marshal(queueTaskData) diff --git a/core/taskengine/vm_runner_rest_test.go b/core/taskengine/vm_runner_rest_test.go index d7e2bea9..5cf0ffda 100644 --- a/core/taskengine/vm_runner_rest_test.go +++ b/core/taskengine/vm_runner_rest_test.go @@ -1,6 +1,7 @@ package taskengine import ( + "errors" "io" "net/http" "net/http/httptest" @@ -341,3 +342,81 @@ func TestRestRequestRenderVarsMultipleExecutions(t *testing.T) { t.Errorf("Headers were modified. Expected %v, got %v", originalHeaders, node.Headers) } } + +func TestRestRequestErrorHandling(t *testing.T) { + node := &avsproto.RestAPINode{ + Url: "http://non-existent-domain-that-will-fail.invalid", + Method: "GET", + } + + nodes := []*avsproto.TaskNode{ + { + Id: "error-test", + Name: "restApi", + TaskType: &avsproto.TaskNode_RestApi{ + RestApi: node, + }, + }, + } + + trigger := &avsproto.TaskTrigger{ + Id: "triggertest", + Name: "triggertest", + } + edges := []*avsproto.TaskEdge{ + { + Id: "e1", + Source: trigger.Id, + Target: "error-test", + }, + } + + vm, err := NewVMWithData(&model.Task{ + &avsproto.Task{ + Id: "error-test", + Nodes: nodes, + Edges: edges, + Trigger: trigger, + }, + }, nil, testutil.GetTestSmartWalletConfig(), nil) + + n := NewRestProrcessor(vm) + + step, err := n.Execute("error-test", node) + + if err == nil { + t.Errorf("expected error for non-existent domain, but got nil") + } + + if !strings.Contains(err.Error(), "HTTP request failed: connection error, timeout, or DNS resolution failure") { + t.Errorf("expected error message to contain connection failure information, got: %v", err) + } + + if step.Success { + t.Errorf("expected step.Success to be false for failed request") + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) // 404 + })) + defer ts.Close() + + node404 := &avsproto.RestAPINode{ + Url: ts.URL, + Method: "GET", + } + + step, err = n.Execute("error-test", node404) + + if err == nil { + t.Errorf("expected error for 404 status code, but got nil") + } + + if !strings.Contains(err.Error(), "unexpected status code: 404") { + t.Errorf("expected error message to contain status code 404, got: %v", err) + } + + if step.Success { + t.Errorf("expected step.Success to be false for 404 response") + } +} From a6c0b1777930a7e2d43a27e1e2f39e7211afe1a5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 04:13:25 +0000 Subject: [PATCH 03/11] Fix unused import in vm_runner_rest_test.go Co-Authored-By: Chris Li --- core/taskengine/vm_runner_rest_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/core/taskengine/vm_runner_rest_test.go b/core/taskengine/vm_runner_rest_test.go index 5cf0ffda..30f1a402 100644 --- a/core/taskengine/vm_runner_rest_test.go +++ b/core/taskengine/vm_runner_rest_test.go @@ -1,7 +1,6 @@ package taskengine import ( - "errors" "io" "net/http" "net/http/httptest" From b30532f25bbdf4f7f178d03be4444cd3e424cb09 Mon Sep 17 00:00:00 2001 From: chrisli30 Date: Fri, 4 Apr 2025 12:20:22 -0700 Subject: [PATCH 04/11] Updated the testing instructions in README.md --- README.md | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b2684252..2bbdb69a 100644 --- a/README.md +++ b/README.md @@ -110,9 +110,37 @@ View docs/development.md ## Testing -The commands to run tests locally are found in the Makefile: -* `go test -race -buildvcs -vet=off ./...` (default) -* `go test -v -race -buildvcs ./...` (verbose) +### Standard Tests +The Makefile includes two primary test configurations: +```bash +# Default test suite +go test -race -buildvcs -vet=off ./... + +# Verbose test output +go test -v -race -buildvcs ./... +``` + +### Enhanced Test Output +For improved test result formatting, use `gotestfmt`: + +1. Install the formatter: +```bash +go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest +``` + +Run once in the current terminal session to make Bash scripts more robust and error-aware. + +```bash +set -euo pipefail +``` + +3. Run tests with formatted output: +```bash +go test -json -run ^TestRestRequestErrorHandling$ ./... 2>&1 | gotestfmt --hide=all +``` + +The `--hide=all` flag suppresses output for skipped and successful tests, showing only failures. For more output configuration options, see the [gotestfmt documentation](https://github.com/GoTestTools/gotestfmt?tab=readme-ov-file#how-do-i-make-the-output-less-verbose). + ======= ## Linting and Code Quality From 1e2ed7de3bf65c303dac8e20f75ea12030b8db65 Mon Sep 17 00:00:00 2001 From: chrisli30 Date: Fri, 4 Apr 2025 13:50:27 -0700 Subject: [PATCH 05/11] Fixed test errors from TestRestRequestErrorHandling --- README.md | 118 ++++++++++++++----------- core/taskengine/vm_runner_rest.go | 59 +++++++------ core/taskengine/vm_runner_rest_test.go | 70 +++++++-------- operator/alias.go | 4 +- operator/operator.go | 2 +- 5 files changed, 134 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 2bbdb69a..74276205 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ Then you can run `ap-avs` binary. We make an effort to use pure Go so you can al Check how to run an [operator docs](docs/operator.md) - ### Run aggregator To run the aggregator, use the following command: @@ -35,7 +34,6 @@ Note: The Ava Protocol team currently manages the aggregator, and the communicat
- ## User wallet For each owner we deploy a ERC6900 wallet to schedule task and approve spending @@ -71,7 +69,6 @@ in the operator config file. - aggregator.avaprotocol.org:2206 - [https://api-explorer.avaprotocol.org/](https://api-explorer.avaprotocol.org/) - ## Operators Operators communicates with aggregators through RPC. It requests task data from aggregator, it performs condition execution to check whether a task can be trigger. The result is then sent back to aggregator. @@ -85,7 +82,7 @@ Currently, Ava Protocol has deployed our operator on the testnet. Community memb ### Testnet -- Ava Protocol's operator: [0x997e5d40a32c44a3d93e59fc55c4fd20b7d2d49d](https://holesky.eigenlayer.xyz/operator/0x997e5d40a32c44a3d93e59fc55c4fd20b7d2d49d). +- Ava Protocol's operator: [0x997e5d40a32c44a3d93e59fc55c4fd20b7d2d49d](https://holesky.eigenlayer.xyz/operator/0x997e5d40a32c44a3d93e59fc55c4fd20b7d2d49d). ### Mainnet @@ -111,7 +108,9 @@ View docs/development.md ## Testing ### Standard Tests + The Makefile includes two primary test configurations: + ```bash # Default test suite go test -race -buildvcs -vet=off ./... @@ -121,27 +120,39 @@ go test -v -race -buildvcs ./... ``` ### Enhanced Test Output + For improved test result formatting, use `gotestfmt`: 1. Install the formatter: -```bash -go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest -``` -Run once in the current terminal session to make Bash scripts more robust and error-aware. + ```bash + go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest + ``` -```bash -set -euo pipefail -``` +2. Run once in the current terminal session to make Bash scripts more robust and error-aware. + + ```bash + set -euo pipefail + ``` 3. Run tests with formatted output: -```bash -go test -json -run ^TestRestRequestErrorHandling$ ./... 2>&1 | gotestfmt --hide=all -``` -The `--hide=all` flag suppresses output for skipped and successful tests, showing only failures. For more output configuration options, see the [gotestfmt documentation](https://github.com/GoTestTools/gotestfmt?tab=readme-ov-file#how-do-i-make-the-output-less-verbose). + Run all tests with complete output: + + ```base + go test -json ./... | gotestfmt + ``` + + or, run selected test cases: + + ```bash + go test -json -run ^TestRestRequestErrorHandling$ ./... 2>&1 | gotestfmt --hide=all + ``` + + The `--hide=all` flag suppresses output for skipped and successful tests, showing only failures. For more output configuration options, see the [gotestfmt documentation](https://github.com/GoTestTools/gotestfmt?tab=readme-ov-file#how-do-i-make-the-output-less-verbose). ======= + ## Linting and Code Quality ### Running the linter @@ -164,7 +175,6 @@ make audit - Include linting in CI/CD pipelines to enforce code quality standards - Fix linting issues as they arise rather than letting them accumulate - ## Dependencies ### EigenLayer CLI @@ -191,6 +201,7 @@ Install the Foundry toolchain with the following commands: curl -L https://foundry.paradigm.xyz | bash foundryup ``` + ### Protobuf Compiler ``` @@ -198,11 +209,12 @@ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest ``` ### Generate API Docs from Proto Files + 1. Install the `protoc-gen-doc` plugin for `protoc`. Install via Go: ``` go install github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc@latest - ``` + ``` Ensure the plugin is in your PATH: ``` export PATH="$PATH:$(go env GOPATH)/bin" @@ -212,20 +224,20 @@ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest which protoc-gen-doc ``` 2. Generate API references in HTML format - ``` - protoc --doc_out=./docs --doc_opt=html,docs.html ./protobuf/avs.proto - ``` -3. Alternatively, generate API references in Markdown format - ``` - protoc --doc_out=./docs --doc_opt=markdown,docs.md ./protobuf/avs.proto - ``` - This command will generate a markdown version of the gRPC API documentation. To enhance the clarity of the generated documentation, the following improvements should be made to the .proto file: - - **Group Definitions by Command**: Organize the API methods and their descriptions by command categories to make it easier for users to find relevant information. - - **Elaborate on Input Fields**: Provide detailed descriptions for each input field, including data types, expected values, and any constraints or special considerations. - - **Add Examples**: Include usage examples for each API method to demonstrate how to construct requests and interpret responses. - - **Link to Related Resources**: Where applicable, link to additional resources or documentation that provide further context or implementation details. + ``` + protoc --doc_out=./docs --doc_opt=html,docs.html ./protobuf/avs.proto + ``` +3. Alternatively, generate API references in Markdown format + ``` + protoc --doc_out=./docs --doc_opt=markdown,docs.md ./protobuf/avs.proto + ``` + This command will generate a markdown version of the gRPC API documentation. To enhance the clarity of the generated documentation, the following improvements should be made to the .proto file: + - **Group Definitions by Command**: Organize the API methods and their descriptions by command categories to make it easier for users to find relevant information. + - **Elaborate on Input Fields**: Provide detailed descriptions for each input field, including data types, expected values, and any constraints or special considerations. + - **Add Examples**: Include usage examples for each API method to demonstrate how to construct requests and interpret responses. + - **Link to Related Resources**: Where applicable, link to additional resources or documentation that provide further context or implementation details. ## Getting started @@ -235,31 +247,29 @@ Coming soon ### Holesky Testnet -| Name | Address | -| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | -| ProxyAdmin | [`0x26CF7A7DF7d1E00D83A5Ca24385f697a3ca4577d`](https://holesky.etherscan.io/address/0x26CF7A7DF7d1E00D83A5Ca24385f697a3ca4577d) | -| ServiceManager | [`0xEA3E82F9Ae371A6a372A6DCffB1a9bD17e0608eF`](https://holesky.etherscan.io/address/0xEA3E82F9Ae371A6a372A6DCffB1a9bD17e0608eF) | -| RegistryCoordinator | [`0x90c6d6f2A78d5Ce22AB8631Ddb142C03AC87De7a`](https://holesky.etherscan.io/address/0x90c6d6f2A78d5Ce22AB8631Ddb142C03AC87De7a) | -| BLSApkRegistry | [`0x6752F8BeeE5BF45c9d11FDBC4F8aFfF879925585`](https://holesky.etherscan.io/address/0x6752F8BeeE5BF45c9d11FDBC4F8aFfF879925585) | -| IndexRegistry | [`0x298a5d3C8F8Db30E8292C9e2BF92292de469C8FF`](https://holesky.etherscan.io/address/0x298a5d3C8F8Db30E8292C9e2BF92292de469C8FF) | -| OperatorStateRetriever | [`0xb7bb920538e038DFFEfcB55caBf713652ED2031F`](https://holesky.etherscan.io/address/0xb7bb920538e038DFFEfcB55caBf713652ED2031F) | -| PauserRegistry | [`0x3A8ea6e4202CdDe4a9e0cCE19c4Dc1739ba2cF0b`](https://holesky.etherscan.io/address/0x3A8ea6e4202CdDe4a9e0cCE19c4Dc1739ba2cF0b) | -| StakeRegistry | [`0x7BacD5dd5A7C3acf8bf1a3c88fB0D00B68EE626A`](https://holesky.etherscan.io/address/0x7BacD5dd5A7C3acf8bf1a3c88fB0D00B68EE626A) | -| ApConfig | [`0xb8abbb082ecaae8d1cd68378cf3b060f6f0e07eb`](https://holesky.etherscan.io/address/0xb8abbb082ecaae8d1cd68378cf3b060f6f0e07eb) | - - +| Name | Address | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| ProxyAdmin | [`0x26CF7A7DF7d1E00D83A5Ca24385f697a3ca4577d`](https://holesky.etherscan.io/address/0x26CF7A7DF7d1E00D83A5Ca24385f697a3ca4577d) | +| ServiceManager | [`0xEA3E82F9Ae371A6a372A6DCffB1a9bD17e0608eF`](https://holesky.etherscan.io/address/0xEA3E82F9Ae371A6a372A6DCffB1a9bD17e0608eF) | +| RegistryCoordinator | [`0x90c6d6f2A78d5Ce22AB8631Ddb142C03AC87De7a`](https://holesky.etherscan.io/address/0x90c6d6f2A78d5Ce22AB8631Ddb142C03AC87De7a) | +| BLSApkRegistry | [`0x6752F8BeeE5BF45c9d11FDBC4F8aFfF879925585`](https://holesky.etherscan.io/address/0x6752F8BeeE5BF45c9d11FDBC4F8aFfF879925585) | +| IndexRegistry | [`0x298a5d3C8F8Db30E8292C9e2BF92292de469C8FF`](https://holesky.etherscan.io/address/0x298a5d3C8F8Db30E8292C9e2BF92292de469C8FF) | +| OperatorStateRetriever | [`0xb7bb920538e038DFFEfcB55caBf713652ED2031F`](https://holesky.etherscan.io/address/0xb7bb920538e038DFFEfcB55caBf713652ED2031F) | +| PauserRegistry | [`0x3A8ea6e4202CdDe4a9e0cCE19c4Dc1739ba2cF0b`](https://holesky.etherscan.io/address/0x3A8ea6e4202CdDe4a9e0cCE19c4Dc1739ba2cF0b) | +| StakeRegistry | [`0x7BacD5dd5A7C3acf8bf1a3c88fB0D00B68EE626A`](https://holesky.etherscan.io/address/0x7BacD5dd5A7C3acf8bf1a3c88fB0D00B68EE626A) | +| ApConfig | [`0xb8abbb082ecaae8d1cd68378cf3b060f6f0e07eb`](https://holesky.etherscan.io/address/0xb8abbb082ecaae8d1cd68378cf3b060f6f0e07eb) | ### Ethereum Mainnet -| Name | Address | -| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | -| ProxyAdmin | [`0x5989934D31f7f397511f105B7E4175a06B7A517F`](https://etherscan.io/address/0x5989934D31f7f397511f105B7E4175a06B7A517F) | -| ServiceManager | [`0x18343Aa10e3D2F3A861e5649627324aEAD987Adf`](https://etherscan.io/address/0x18343Aa10e3D2F3A861e5649627324aEAD987Adf) | -| RegistryCoordinator | [`0x8DE3Ee0dE880161Aa0CD8Bf9F8F6a7AfEeB9A44B`](https://etherscan.io/address/0x8DE3Ee0dE880161Aa0CD8Bf9F8F6a7AfEeB9A44B) | -| BLSApkRegistry | [`0xB58687fF303C8e92C28a484342755d3228081d45`](https://etherscan.io/address/0xB58687fF303C8e92C28a484342755d3228081d45) | -| IndexRegistry | [`0xc6A464e39d4fA5013D61295501c7cCd050d76612`](https://etherscan.io/address/0xc6A464e39d4fA5013D61295501c7cCd050d76612) | -| OperatorStateRetriever | [`0xb3af70D5f72C04D1f490ff49e5aB189fA7122713`](https://etherscan.io/address/0xb3af70D5f72C04D1f490ff49e5aB189fA7122713) | -| PauserRegistry | [`0xeec585186c37c517030ba371deac5c17e728c135`](https://etherscan.io/address/0xeec585186c37c517030ba371deac5c17e728c135) | -| StakeRegistry | [`0x363b3604fE8c2323a98c00906115c8b87a512a12`](https://etherscan.io/address/0x363b3604fE8c2323a98c00906115c8b87a512a12) | -| TaskManager | [`0x940f62f75cbbbd723d37c9171dc681dfba653b49`](https://etherscan.io/address/0x940f62f75cbbbd723d37c9171dc681dfba653b49) | -| ApConfig | [`0x9c02dfc92eea988902a98919bf4f035e4aaefced`](https://etherscan.io/address/0x9c02dfc92eea988902a98919bf4f035e4aaefced) | +| Name | Address | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| ProxyAdmin | [`0x5989934D31f7f397511f105B7E4175a06B7A517F`](https://etherscan.io/address/0x5989934D31f7f397511f105B7E4175a06B7A517F) | +| ServiceManager | [`0x18343Aa10e3D2F3A861e5649627324aEAD987Adf`](https://etherscan.io/address/0x18343Aa10e3D2F3A861e5649627324aEAD987Adf) | +| RegistryCoordinator | [`0x8DE3Ee0dE880161Aa0CD8Bf9F8F6a7AfEeB9A44B`](https://etherscan.io/address/0x8DE3Ee0dE880161Aa0CD8Bf9F8F6a7AfEeB9A44B) | +| BLSApkRegistry | [`0xB58687fF303C8e92C28a484342755d3228081d45`](https://etherscan.io/address/0xB58687fF303C8e92C28a484342755d3228081d45) | +| IndexRegistry | [`0xc6A464e39d4fA5013D61295501c7cCd050d76612`](https://etherscan.io/address/0xc6A464e39d4fA5013D61295501c7cCd050d76612) | +| OperatorStateRetriever | [`0xb3af70D5f72C04D1f490ff49e5aB189fA7122713`](https://etherscan.io/address/0xb3af70D5f72C04D1f490ff49e5aB189fA7122713) | +| PauserRegistry | [`0xeec585186c37c517030ba371deac5c17e728c135`](https://etherscan.io/address/0xeec585186c37c517030ba371deac5c17e728c135) | +| StakeRegistry | [`0x363b3604fE8c2323a98c00906115c8b87a512a12`](https://etherscan.io/address/0x363b3604fE8c2323a98c00906115c8b87a512a12) | +| TaskManager | [`0x940f62f75cbbbd723d37c9171dc681dfba653b49`](https://etherscan.io/address/0x940f62f75cbbbd723d37c9171dc681dfba653b49) | +| ApConfig | [`0x9c02dfc92eea988902a98919bf4f035e4aaefced`](https://etherscan.io/address/0x9c02dfc92eea988902a98919bf4f035e4aaefced) | diff --git a/core/taskengine/vm_runner_rest.go b/core/taskengine/vm_runner_rest.go index 459ca5c3..869c4c46 100644 --- a/core/taskengine/vm_runner_rest.go +++ b/core/taskengine/vm_runner_rest.go @@ -2,6 +2,7 @@ package taskengine import ( "encoding/json" + "errors" "fmt" "net/url" "strings" @@ -46,7 +47,7 @@ func (r *RestProcessor) Execute(stepID string, node *avsproto.RestAPINode) (*avs NodeId: stepID, Log: "", OutputData: nil, - Success: true, + Success: true, // Start optimistically Error: "", StartAt: t0, } @@ -80,10 +81,6 @@ func (r *RestProcessor) Execute(stepID string, node *avsproto.RestAPINode) (*avs var err error defer func() { s.EndAt = time.Now().UnixMilli() - s.Success = err == nil - if err != nil { - s.Error = err.Error() - } }() var log strings.Builder @@ -97,8 +94,9 @@ func (r *RestProcessor) Execute(stepID string, node *avsproto.RestAPINode) (*avs u, err := url.Parse(processedNode.Url) if err != nil { - s.Error = fmt.Sprintf("cannot parse url: %s", processedNode.Url) - return nil, err + s.Success = false + s.Error = fmt.Sprintf("Cannot parse URL: %s", processedNode.Url) + return s, err } log.WriteString(fmt.Sprintf("Execute %s %s at %s", processedNode.Method, u.Hostname(), time.Now())) @@ -115,9 +113,22 @@ func (r *RestProcessor) Execute(stepID string, node *avsproto.RestAPINode) (*avs resp, err = request.Get(processedNode.Url) } - response := string(resp.Body()) + // Handle connection errors + if err != nil { + s.Success = false + s.Error = "HTTP request failed: connection error, timeout, or DNS resolution failure" + return s, fmt.Errorf("%s: %v", s.Error, err) + } - //maybeJSON := false + // Check HTTP status code - treat non-2xx/3xx as errors + if resp.StatusCode() < 200 || resp.StatusCode() >= 400 { + s.Success = false + errorMsg := fmt.Sprintf("Unexpected status code: %d", resp.StatusCode()) + s.Error = errorMsg + return s, errors.New(errorMsg) + } + + response := string(resp.Body()) // Attempt to detect json and auto convert to a map to use in subsequent step if len(response) >= 1 && (response[0] == '{' || response[0] == '[') { @@ -132,31 +143,25 @@ func (r *RestProcessor) Execute(stepID string, node *avsproto.RestAPINode) (*avs } value, err := structpb.NewValue(r.GetOutputVar(stepID)) - if err == nil { - pbResult, _ := anypb.New(value) - s.OutputData = &avsproto.Execution_Step_RestApi{ - RestApi: &avsproto.RestAPINode_Output{ - Data: pbResult, - }, - } + if err != nil { + s.Success = false + s.Error = err.Error() + return s, err } + pbResult, err := anypb.New(value) if err != nil { s.Success = false s.Error = err.Error() return s, err - } else { - // Check if the response status code is not 2xx or 3xx, we consider it as an error exeuction - if resp.StatusCode() < 200 || resp.StatusCode() >= 400 { - s.Success = false - errorMsg := fmt.Sprintf("unexpected status code: %d", resp.StatusCode()) - if resp.StatusCode() == 0 { - errorMsg = "HTTP request failed: connection error, timeout, or DNS resolution failure (status code: 0)" - } - s.Error = errorMsg - return s, fmt.Errorf(errorMsg) - } } + s.OutputData = &avsproto.Execution_Step_RestApi{ + RestApi: &avsproto.RestAPINode_Output{ + Data: pbResult, + }, + } + + // If we reach here, everything was successful return s, nil } diff --git a/core/taskengine/vm_runner_rest_test.go b/core/taskengine/vm_runner_rest_test.go index 30f1a402..d9a151db 100644 --- a/core/taskengine/vm_runner_rest_test.go +++ b/core/taskengine/vm_runner_rest_test.go @@ -60,34 +60,34 @@ func TestRestRequest(t *testing.T) { step, err := n.Execute("123abc", node) if err != nil { - t.Errorf("expected rest node run succesfull but got error: %v", err) + t.Errorf("Expected rest node run successful but got error: %v", err) } if !step.Success { - t.Errorf("expected rest node run succesfully but failed") + t.Errorf("Expected rest node run successfully but failed") } if !strings.Contains(step.Log, "Execute POST httpbin.org at") { - t.Errorf("expected log contains request trace data but found no") + t.Errorf("Expected log to contain request trace data but found no") } if step.Error != "" { - t.Errorf("expected log contains request trace data but found no") + t.Errorf("Expected log to contain request trace data but found no") } outputData := gow.AnyToMap(step.GetRestApi().Data)["form"].(map[string]any) //[chat_id:123 disable_notification:true text:*This is a test format*] if outputData["chat_id"].(string) != "123" { - t.Errorf("expect chat_id is 123 but got: %s", outputData["chat_id"]) + t.Errorf("Expected chat_id to be 123 but got: %s", outputData["chat_id"]) } if outputData["text"].(string) != "*This is a test format*" { - t.Errorf("expect text is *This is a test format* but got: %s", outputData["text"]) + t.Errorf("Expected text to be *This is a test format* but got: %s", outputData["text"]) } if outputData["disable_notification"].(string) != "true" { - t.Errorf("expect notificaion is disable but got: %s", outputData["disable_notification"]) + t.Errorf("Expected notification to be disabled but got: %s", outputData["disable_notification"]) } } @@ -95,7 +95,7 @@ func TestRestRequestHandleEmptyResponse(t *testing.T) { // Create test server ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - t.Errorf("expected POST request, got %s", r.Method) + t.Errorf("Expected POST request, got %s", r.Method) } w.Header().Set("Content-Type", "application/json") w.Write([]byte("")) @@ -147,15 +147,15 @@ func TestRestRequestHandleEmptyResponse(t *testing.T) { step, err := n.Execute("123abc", node) if err != nil { - t.Errorf("expected rest node run succesfull but got error: %v", err) + t.Errorf("Expected rest node run successful but got error: %v", err) } if !step.Success { - t.Errorf("expected rest node run succesfully but failed") + t.Errorf("Expected rest node run successfully but failed") } if gow.AnyToString(step.GetRestApi().Data) != "" { - t.Errorf("expected an empty response, got: %s", step.OutputData) + t.Errorf("Expected an empty response, got: %s", step.OutputData) } } @@ -163,7 +163,7 @@ func TestRestRequestRenderVars(t *testing.T) { // Create test server ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - t.Errorf("expected POST request, got %s", r.Method) + t.Errorf("Expected POST request, got %s", r.Method) } w.Header().Set("Content-Type", "application/json") body, _ := io.ReadAll(r.Body) @@ -212,8 +212,8 @@ func TestRestRequestRenderVars(t *testing.T) { }, nil, testutil.GetTestSmartWalletConfig(), nil) vm.AddVar("myNode", map[string]map[string]string{ - "data": { - "name": "unitest", + "data": map[string]string{ + "name": "unit test", }, }) @@ -222,15 +222,15 @@ func TestRestRequestRenderVars(t *testing.T) { step, err := n.Execute("123abc", node) if err != nil { - t.Errorf("expected rest node run succesfull but got error: %v", err) + t.Errorf("Expected rest node run successful but got error: %v", err) } if !step.Success { - t.Errorf("expected rest node run succesfully but failed") + t.Errorf("Expected rest node run successfully but failed") } - if gow.AnyToString(step.GetRestApi().Data) != "my name is unitest" { - t.Errorf("expected response is `my name is unitest`, got: %s", step.OutputData) + if gow.AnyToString(step.GetRestApi().Data) != "my name is unit test" { + t.Errorf("Expected response to be 'my name is unit test', got: %s", step.OutputData) } } @@ -238,7 +238,7 @@ func TestRestRequestRenderVarsMultipleExecutions(t *testing.T) { // Create test server ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - t.Errorf("expected POST request, got %s", r.Method) + t.Errorf("Expected POST request, got %s", r.Method) } w.Header().Set("Content-Type", "application/json") body, _ := io.ReadAll(r.Body) @@ -302,13 +302,13 @@ func TestRestRequestRenderVarsMultipleExecutions(t *testing.T) { step, err := n.Execute("123abc", node) if err != nil { - t.Errorf("expected rest node run successful but got error: %v", err) + t.Errorf("Expected rest node run successful but got error: %v", err) } if !step.Success { - t.Errorf("expected rest node run successfully but failed") + t.Errorf("Expected rest node run successfully but failed") } if gow.AnyToString(step.GetRestApi().Data) != "my name is first" { - t.Errorf("expected response is `my name is first`, got: %s", step.OutputData) + t.Errorf("Expected response to be 'my name is first', got: %s", step.OutputData) } // Second execution with different value @@ -321,24 +321,24 @@ func TestRestRequestRenderVarsMultipleExecutions(t *testing.T) { step, err = n.Execute("123abc", node) if err != nil { - t.Errorf("expected rest node run successful but got error: %v", err) + t.Errorf("Expected rest node run successful but got error: %v", err) } if !step.Success { - t.Errorf("expected rest node run successfully but failed") + t.Errorf("Expected rest node run successfully but failed") } if gow.AnyToString(step.GetRestApi().Data) != "my name is second" { - t.Errorf("expected response is `my name is second`, got: %s", step.OutputData) + t.Errorf("Expected response to be 'my name is second', got: %s", step.OutputData) } // Verify original node values remain unchanged if node.Url != originalUrl { - t.Errorf("URL was modified. Expected %s, got %s", originalUrl, node.Url) + t.Errorf("Expected URL to be %s, got %s", originalUrl, node.Url) } if node.Body != originalBody { - t.Errorf("Body was modified. Expected %s, got %s", originalBody, node.Body) + t.Errorf("Expected Body to be %s, got %s", originalBody, node.Body) } if !reflect.DeepEqual(node.Headers, originalHeaders) { - t.Errorf("Headers were modified. Expected %v, got %v", originalHeaders, node.Headers) + t.Errorf("Expected Headers to be %v, got %v", originalHeaders, node.Headers) } } @@ -384,15 +384,15 @@ func TestRestRequestErrorHandling(t *testing.T) { step, err := n.Execute("error-test", node) if err == nil { - t.Errorf("expected error for non-existent domain, but got nil") + t.Errorf("Expected error for non-existent domain, but got nil") } if !strings.Contains(err.Error(), "HTTP request failed: connection error, timeout, or DNS resolution failure") { - t.Errorf("expected error message to contain connection failure information, got: %v", err) + t.Errorf("Expected error message to contain connection failure information, got: %v", err) } if step.Success { - t.Errorf("expected step.Success to be false for failed request") + t.Errorf("Expected step.Success to be false for failed request") } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -408,14 +408,14 @@ func TestRestRequestErrorHandling(t *testing.T) { step, err = n.Execute("error-test", node404) if err == nil { - t.Errorf("expected error for 404 status code, but got nil") + t.Errorf("Expected error for 404 status code, but got nil") } - if !strings.Contains(err.Error(), "unexpected status code: 404") { - t.Errorf("expected error message to contain status code 404, got: %v", err) + if !strings.Contains(err.Error(), "Unexpected status code: 404") { + t.Errorf("Expected error message to contain status code 404, got: %v", err) } if step.Success { - t.Errorf("expected step.Success to be false for 404 response") + t.Errorf("Expected step.Success to be false for 404 response") } } diff --git a/operator/alias.go b/operator/alias.go index ffeb98e6..b230df19 100644 --- a/operator/alias.go +++ b/operator/alias.go @@ -104,7 +104,7 @@ func (o *Operator) DeclareAlias(filepath string) error { } if receipt.Status != 1 { - return fmt.Errorf("declareAlias transaction %w reverted", receipt.TxHash.Hex()) + return fmt.Errorf("declareAlias transaction %s reverted", receipt.TxHash.Hex()) } fmt.Printf("succesfully declared an alias for operator %s alias address %s at tx %s ", o.operatorAddr.String(), crypto.PubkeyToAddress(aliasEcdsaPair.PublicKey), receipt.TxHash.Hex()) @@ -151,7 +151,7 @@ func (o *Operator) RemoveAlias() error { } if receipt.Status != 1 { - return fmt.Errorf("declareAlias transaction %w reverted", receipt.TxHash.Hex()) + return fmt.Errorf("declareAlias transaction %s reverted", receipt.TxHash.Hex()) } fmt.Printf("succesfully remove alias %s for operator %s at tx %s ", o.signerAddress.String(), o.operatorAddr.String(), receipt.TxHash.Hex()) diff --git a/operator/operator.go b/operator/operator.go index 62301f1a..87c7e863 100644 --- a/operator/operator.go +++ b/operator/operator.go @@ -169,7 +169,7 @@ func NewOperatorFromConfigFile(configPath string) (*Operator, error) { err := config.ReadYamlConfig(configPath, &nodeConfig) if err != nil { - panic(fmt.Errorf("failed to parse config file: %w\nMake sure %s is exist and a valid yaml file %w.", configPath, err)) + panic(fmt.Errorf("failed to parse config file: %w\nMake sure %s exists and is a valid yaml file", err, configPath)) } return NewOperatorFromConfig(nodeConfig) From c18765ff372de893c57e85cfb88f7aab23b70486 Mon Sep 17 00:00:00 2001 From: chrisli30 Date: Fri, 4 Apr 2025 14:49:26 -0700 Subject: [PATCH 06/11] Added Paymaster contract address to docs/contract.md --- docs/contract.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contract.md b/docs/contract.md index 9d0cc79e..62a31c39 100644 --- a/docs/contract.md +++ b/docs/contract.md @@ -22,7 +22,7 @@ We use a consistent contract address across four networks: | **Wallet Implementation** | `0x552D410C9c4231841413F6061baaCB5c8fBFB0DE` | [View](https://sepolia.basescan.org/address/0x552D410C9c4231841413F6061baaCB5c8fBFB0DE) | [View](https://basescan.org/address/0x552D410C9c4231841413F6061baaCB5c8fBFB0DE) | [View](https://sepolia.etherscan.io/address/0x552D410C9c4231841413F6061baaCB5c8fBFB0DE) | [View](https://etherscan.io/address/0x552D410C9c4231841413F6061baaCB5c8fBFB0DE) | | **Factory Proxy** | `0xB99BC2E399e06CddCF5E725c0ea341E8f0322834` | [View](https://sepolia.basescan.org/address/0xB99BC2E399e06CddCF5E725c0ea341E8f0322834) | [View](https://basescan.org/address/0xB99BC2E399e06CddCF5E725c0ea341E8f0322834) | [View](https://sepolia.etherscan.io/address/0xB99BC2E399e06CddCF5E725c0ea341E8f0322834) | [View](https://etherscan.io/address/0xB99BC2E399e06CddCF5E725c0ea341E8f0322834) | | **Factory Implementation** | `0x5692D03FC5922b806F382E4F1A620479A14c96c2` | [View](https://sepolia.basescan.org/address/0x5692D03FC5922b806F382E4F1A620479A14c96c2) | [View](https://basescan.org/address/0x5692D03FC5922b806F382E4F1A620479A14c96c2) | [View](https://sepolia.etherscan.io/address/0x5692D03FC5922b806F382E4F1A620479A14c96c2) | [View](https://etherscan.io/address/0x5692D03FC5922b806F382E4F1A620479A14c96c2) | - +| **Paymaster Contract** | `0xB985af5f96EF2722DC99aEBA573520903B86505e` | [View](https://sepolia.basescan.org/address/0xB985af5f96EF2722DC99aEBA573520903B86505e) | [View](https://basescan.org/address/0xB985af5f96EF2722DC99aEBA573520903B86505e) | [View](https://sepolia.etherscan.io/address/0xB985af5f96EF2722DC99aEBA573520903B86505e) | [View](https://etherscan.io/address/0xB985af5f96EF2722DC99aEBA573520903B86505e) | ### Pre-fund The first transaction require siginficant higher gas to pay for contract deployment. Below is the sample pre-fund requirement to the smart contract. The smart contract address of wallet can compute ahead of time From 4876da1029931903738238ece541059bbf62e031 Mon Sep 17 00:00:00 2001 From: chrisli30 Date: Fri, 4 Apr 2025 15:02:32 -0700 Subject: [PATCH 07/11] Added fixes for TestContractWriteSimpleReturn case --- core/taskengine/vm_runner_contract_write.go | 67 +++++++++---------- .../vm_runner_contract_write_test.go | 2 +- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/core/taskengine/vm_runner_contract_write.go b/core/taskengine/vm_runner_contract_write.go index 9389c93e..d0493f63 100644 --- a/core/taskengine/vm_runner_contract_write.go +++ b/core/taskengine/vm_runner_contract_write.go @@ -77,11 +77,11 @@ func (r *ContractWriteProcessor) Execute(stepID string, node *avsproto.ContractW log.WriteString("\nsend userops to bundler rpc\n") total, _ := r.vm.db.GetCounter(ContractWriteCounterKey(r.owner), 0) - + var paymasterRequest *preset.VerifyingPaymasterRequest // TODO: move to config if total < 10 { - paymasterRequest = preset.GetVerifyingPaymasterRequestForDuration(r.smartWalletConfig.PaymasterAddress, 15 * time.Minute) + paymasterRequest = preset.GetVerifyingPaymasterRequestForDuration(r.smartWalletConfig.PaymasterAddress, 15*time.Minute) } userOp, txReceipt, err := preset.SendUserOp( @@ -97,17 +97,6 @@ func (r *ContractWriteProcessor) Execute(stepID string, node *avsproto.ContractW } r.vm.db.IncCounter(ContractWriteCounterKey(r.owner), 0) - var bloom []byte - if txReceipt != nil { - bloom, _ = txReceipt.Bloom.MarshalText() - } - - blobGasPrice := uint64(0) - - if txReceipt != nil && txReceipt.BlobGasPrice != nil { - blobGasPrice = uint64(txReceipt.BlobGasPrice.Int64()) - } - outputData := &avsproto.Execution_Step_ContractWrite{ ContractWrite: &avsproto.ContractWriteNode_Output{ UserOp: &avsproto.Evm_UserOp{ @@ -123,31 +112,37 @@ func (r *ContractWriteProcessor) Execute(stepID string, node *avsproto.ContractW PaymasterAndData: common.Bytes2Hex(userOp.PaymasterAndData), Signature: common.Bytes2Hex(userOp.Signature), }, - - TxReceipt: &avsproto.Evm_TransactionReceipt{ - Hash: txReceipt.TxHash.Hex(), - BlockHash: txReceipt.BlockHash.Hex(), - BlockNumber: uint64(txReceipt.BlockNumber.Int64()), - // TODO: Need to fetch this, it isn't available - //From: txReceipt.From.Hex(), - //To: txReceipt.To.Hex(), - GasUsed: txReceipt.GasUsed, - GasPrice: uint64(txReceipt.EffectiveGasPrice.Int64()), - CumulativeGasUsed: txReceipt.CumulativeGasUsed, - // Fee: txReceipt.Fee, - ContractAddress: txReceipt.ContractAddress.Hex(), - Index: uint64(txReceipt.TransactionIndex), - // TODO: convert raw log - //Logs: txReceipt.Logs, - LogsBloom: common.Bytes2Hex(bloom), - Root: common.Bytes2Hex(txReceipt.PostState), - Status: uint32(txReceipt.Status), - Type: uint32(txReceipt.Type), - BlobGasPrice: blobGasPrice, - BlobGasUsed: uint64(txReceipt.BlobGasUsed), - }, }, } + + // Only add TxReceipt if it exists + if txReceipt != nil { + var bloom []byte + bloom, _ = txReceipt.Bloom.MarshalText() + + blobGasPrice := uint64(0) + if txReceipt.BlobGasPrice != nil { + blobGasPrice = uint64(txReceipt.BlobGasPrice.Int64()) + } + + outputData.ContractWrite.TxReceipt = &avsproto.Evm_TransactionReceipt{ + Hash: txReceipt.TxHash.Hex(), + BlockHash: txReceipt.BlockHash.Hex(), + BlockNumber: uint64(txReceipt.BlockNumber.Int64()), + GasUsed: txReceipt.GasUsed, + GasPrice: uint64(txReceipt.EffectiveGasPrice.Int64()), + CumulativeGasUsed: txReceipt.CumulativeGasUsed, + ContractAddress: txReceipt.ContractAddress.Hex(), + Index: uint64(txReceipt.TransactionIndex), + LogsBloom: common.Bytes2Hex(bloom), + Root: common.Bytes2Hex(txReceipt.PostState), + Status: uint32(txReceipt.Status), + Type: uint32(txReceipt.Type), + BlobGasPrice: blobGasPrice, + BlobGasUsed: uint64(txReceipt.BlobGasUsed), + } + } + s.OutputData = outputData r.SetOutputVarForStep(stepID, map[string]any{ "userOp": outputData.ContractWrite.UserOp, diff --git a/core/taskengine/vm_runner_contract_write_test.go b/core/taskengine/vm_runner_contract_write_test.go index 424b3768..60739dcf 100644 --- a/core/taskengine/vm_runner_contract_write_test.go +++ b/core/taskengine/vm_runner_contract_write_test.go @@ -50,7 +50,7 @@ func TestContractWriteSimpleReturn(t *testing.T) { } vm, err := NewVMWithData(&model.Task{ - &avsproto.Task{ + Task: &avsproto.Task{ Id: "query1", Nodes: nodes, Edges: edges, From 9555d287c4f519c6981c8dd938dd308f2397c3b0 Mon Sep 17 00:00:00 2001 From: chrisli30 Date: Fri, 4 Apr 2025 15:11:16 -0700 Subject: [PATCH 08/11] Updated error message in operator/operator.go --- operator/operator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator/operator.go b/operator/operator.go index 87c7e863..63ebaf19 100644 --- a/operator/operator.go +++ b/operator/operator.go @@ -169,7 +169,7 @@ func NewOperatorFromConfigFile(configPath string) (*Operator, error) { err := config.ReadYamlConfig(configPath, &nodeConfig) if err != nil { - panic(fmt.Errorf("failed to parse config file: %w\nMake sure %s exists and is a valid yaml file", err, configPath)) + panic(fmt.Errorf("failed to parse config file: %s\nMake sure it exists and is a valid yaml file %w", configPath, err)) } return NewOperatorFromConfig(nodeConfig) From c00cbc552b2473fec252c27a2862f8f93b1381f6 Mon Sep 17 00:00:00 2001 From: chrisli30 Date: Fri, 4 Apr 2025 23:56:29 -0700 Subject: [PATCH 09/11] Added missing txReceipt fields to vm_runner_contract_write.go and test their existence. --- core/taskengine/vm_runner_contract_write.go | 29 ++++++++ .../vm_runner_contract_write_test.go | 67 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/core/taskengine/vm_runner_contract_write.go b/core/taskengine/vm_runner_contract_write.go index d0493f63..dcdc5934 100644 --- a/core/taskengine/vm_runner_contract_write.go +++ b/core/taskengine/vm_runner_contract_write.go @@ -1,12 +1,15 @@ package taskengine import ( + "context" + "encoding/json" "fmt" "math/big" "strings" "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/AvaProtocol/ap-avs/core/chainio/aa" @@ -125,15 +128,32 @@ func (r *ContractWriteProcessor) Execute(stepID string, node *avsproto.ContractW blobGasPrice = uint64(txReceipt.BlobGasPrice.Int64()) } + // Get the transaction to access From and To fields + tx, _, err := r.client.TransactionByHash(context.Background(), txReceipt.TxHash) + if err != nil { + return nil, fmt.Errorf("failed to get transaction: %w", err) + } + + // Get the sender address using the newer method + signer := types.LatestSignerForChainID(tx.ChainId()) + from, err := types.Sender(signer, tx) + if err != nil { + return nil, fmt.Errorf("failed to get sender from transaction: %w", err) + } + outputData.ContractWrite.TxReceipt = &avsproto.Evm_TransactionReceipt{ Hash: txReceipt.TxHash.Hex(), BlockHash: txReceipt.BlockHash.Hex(), BlockNumber: uint64(txReceipt.BlockNumber.Int64()), + From: from.Hex(), + To: tx.To().Hex(), GasUsed: txReceipt.GasUsed, GasPrice: uint64(txReceipt.EffectiveGasPrice.Int64()), CumulativeGasUsed: txReceipt.CumulativeGasUsed, + Fee: uint64(txReceipt.GasUsed * txReceipt.EffectiveGasPrice.Uint64()), ContractAddress: txReceipt.ContractAddress.Hex(), Index: uint64(txReceipt.TransactionIndex), + Logs: make([]string, len(txReceipt.Logs)), LogsBloom: common.Bytes2Hex(bloom), Root: common.Bytes2Hex(txReceipt.PostState), Status: uint32(txReceipt.Status), @@ -141,6 +161,15 @@ func (r *ContractWriteProcessor) Execute(stepID string, node *avsproto.ContractW BlobGasPrice: blobGasPrice, BlobGasUsed: uint64(txReceipt.BlobGasUsed), } + + // Convert logs to JSON strings for storage in the protobuf message + for i, log := range txReceipt.Logs { + logBytes, err := json.Marshal(log) + if err != nil { + return nil, fmt.Errorf("failed to marshal log: %w", err) + } + outputData.ContractWrite.TxReceipt.Logs[i] = string(logBytes) + } } s.OutputData = outputData diff --git a/core/taskengine/vm_runner_contract_write_test.go b/core/taskengine/vm_runner_contract_write_test.go index 60739dcf..abd45e72 100644 --- a/core/taskengine/vm_runner_contract_write_test.go +++ b/core/taskengine/vm_runner_contract_write_test.go @@ -103,7 +103,74 @@ func TestContractWriteSimpleReturn(t *testing.T) { return } + // Print logs for debugging + t.Logf("Logs: %+v", outputData.TxReceipt.Logs) + if len(outputData.TxReceipt.Hash) != 66 { t.Errorf("Missing Tx Hash in the output data") } + + // Verify all transaction receipt fields + if outputData.TxReceipt.BlockHash == "" { + t.Errorf("Missing BlockHash in the output data") + } + + if outputData.TxReceipt.BlockNumber == 0 { + t.Errorf("Missing BlockNumber in the output data") + } + + if outputData.TxReceipt.From == "" { + t.Errorf("Missing From address in the output data") + } + + if outputData.TxReceipt.To == "" { + t.Errorf("Missing To address in the output data") + } + + if outputData.TxReceipt.GasUsed == 0 { + t.Errorf("Missing GasUsed in the output data") + } + + if outputData.TxReceipt.GasPrice == 0 { + t.Errorf("Missing GasPrice in the output data") + } + + if outputData.TxReceipt.CumulativeGasUsed == 0 { + t.Errorf("Missing CumulativeGasUsed in the output data") + } + + if outputData.TxReceipt.Fee == 0 { + t.Errorf("Missing Fee in the output data") + } + + if outputData.TxReceipt.ContractAddress == "" { + t.Errorf("Missing ContractAddress in the output data") + } + + if outputData.TxReceipt.Index == 0 { + t.Errorf("Missing Index in the output data") + } + + if outputData.TxReceipt.Logs == nil { + t.Errorf("Missing Logs in the output data") + } + + if outputData.TxReceipt.LogsBloom == "" { + t.Errorf("Missing LogsBloom in the output data") + } + + // Root is optional in modern Ethereum, only used in pre-Byzantium hard forks + // if outputData.TxReceipt.Root == "" { + // t.Errorf("Missing Root in the output data") + // } + + if outputData.TxReceipt.Status == 0 { + t.Errorf("Missing Status in the output data") + } + + if outputData.TxReceipt.Type == 0 { + t.Errorf("Missing Type in the output data") + } + + // BlobGasPrice and BlobGasUsed are optional fields, so we don't check them } From 2cfbb48616483b169f386dfcb89452781601b480 Mon Sep 17 00:00:00 2001 From: chrisli30 Date: Sat, 5 Apr 2025 00:10:10 -0700 Subject: [PATCH 10/11] Updated vm_runner_rest.go to handle status 0 --- core/taskengine/vm_runner_rest.go | 66 +++++++++++++------------- core/taskengine/vm_runner_rest_test.go | 39 ++++++++++++--- 2 files changed, 66 insertions(+), 39 deletions(-) diff --git a/core/taskengine/vm_runner_rest.go b/core/taskengine/vm_runner_rest.go index 869c4c46..3de0f2f2 100644 --- a/core/taskengine/vm_runner_rest.go +++ b/core/taskengine/vm_runner_rest.go @@ -2,7 +2,6 @@ package taskengine import ( "encoding/json" - "errors" "fmt" "net/url" "strings" @@ -79,8 +78,13 @@ func (r *RestProcessor) Execute(stepID string, node *avsproto.RestAPINode) (*avs } var err error + // The defer function serves as the single source of truth for setting Success: false defer func() { s.EndAt = time.Now().UnixMilli() + s.Success = err == nil + if err != nil { + s.Error = err.Error() + } }() var log strings.Builder @@ -94,9 +98,8 @@ func (r *RestProcessor) Execute(stepID string, node *avsproto.RestAPINode) (*avs u, err := url.Parse(processedNode.Url) if err != nil { - s.Success = false - s.Error = fmt.Sprintf("Cannot parse URL: %s", processedNode.Url) - return s, err + s.Error = fmt.Sprintf("cannot parse url: %s", processedNode.Url) + return nil, err } log.WriteString(fmt.Sprintf("Execute %s %s at %s", processedNode.Method, u.Hostname(), time.Now())) @@ -113,23 +116,10 @@ func (r *RestProcessor) Execute(stepID string, node *avsproto.RestAPINode) (*avs resp, err = request.Get(processedNode.Url) } - // Handle connection errors - if err != nil { - s.Success = false - s.Error = "HTTP request failed: connection error, timeout, or DNS resolution failure" - return s, fmt.Errorf("%s: %v", s.Error, err) - } - - // Check HTTP status code - treat non-2xx/3xx as errors - if resp.StatusCode() < 200 || resp.StatusCode() >= 400 { - s.Success = false - errorMsg := fmt.Sprintf("Unexpected status code: %d", resp.StatusCode()) - s.Error = errorMsg - return s, errors.New(errorMsg) - } - response := string(resp.Body()) + //maybeJSON := false + // Attempt to detect json and auto convert to a map to use in subsequent step if len(response) >= 1 && (response[0] == '{' || response[0] == '[') { var parseData map[string]any @@ -143,25 +133,37 @@ func (r *RestProcessor) Execute(stepID string, node *avsproto.RestAPINode) (*avs } value, err := structpb.NewValue(r.GetOutputVar(stepID)) - if err != nil { - s.Success = false - s.Error = err.Error() - return s, err + if err == nil { + pbResult, _ := anypb.New(value) + s.OutputData = &avsproto.Execution_Step_RestApi{ + RestApi: &avsproto.RestAPINode_Output{ + Data: pbResult, + }, + } } - pbResult, err := anypb.New(value) if err != nil { - s.Success = false s.Error = err.Error() return s, err + } else { + // Check HTTP status codes from the resty response + // - 2xx (200-299): Success + // - 3xx (300-399): Redirection (also considered successful) + // - 4xx (400-499): Client errors + // - 5xx (500-599): Server errors + // Any status code outside 2xx-3xx range is considered an error + // Status code 0 indicates a connection failure + if resp.StatusCode() == 0 { + err = fmt.Errorf("HTTP request failed: connection error or timeout") + s.Error = err.Error() + return s, err + } + if resp.StatusCode() < 200 || resp.StatusCode() >= 400 { + err = fmt.Errorf("unexpected HTTP status code: %d", resp.StatusCode()) + s.Error = err.Error() + return s, err + } } - s.OutputData = &avsproto.Execution_Step_RestApi{ - RestApi: &avsproto.RestAPINode_Output{ - Data: pbResult, - }, - } - - // If we reach here, everything was successful return s, nil } diff --git a/core/taskengine/vm_runner_rest_test.go b/core/taskengine/vm_runner_rest_test.go index d9a151db..86877efb 100644 --- a/core/taskengine/vm_runner_rest_test.go +++ b/core/taskengine/vm_runner_rest_test.go @@ -47,7 +47,7 @@ func TestRestRequest(t *testing.T) { } vm, err := NewVMWithData(&model.Task{ - &avsproto.Task{ + Task: &avsproto.Task{ Id: "123abc", Nodes: nodes, Edges: edges, @@ -134,7 +134,7 @@ func TestRestRequestHandleEmptyResponse(t *testing.T) { } vm, err := NewVMWithData(&model.Task{ - &avsproto.Task{ + Task: &avsproto.Task{ Id: "123abc", Nodes: nodes, Edges: edges, @@ -203,7 +203,7 @@ func TestRestRequestRenderVars(t *testing.T) { } vm, err := NewVMWithData(&model.Task{ - &avsproto.Task{ + Task: &avsproto.Task{ Id: "123abc", Nodes: nodes, Edges: edges, @@ -283,7 +283,7 @@ func TestRestRequestRenderVarsMultipleExecutions(t *testing.T) { } vm, err := NewVMWithData(&model.Task{ - &avsproto.Task{ + Task: &avsproto.Task{ Id: "123abc", Nodes: nodes, Edges: edges, @@ -371,7 +371,7 @@ func TestRestRequestErrorHandling(t *testing.T) { } vm, err := NewVMWithData(&model.Task{ - &avsproto.Task{ + Task: &avsproto.Task{ Id: "error-test", Nodes: nodes, Edges: edges, @@ -387,7 +387,7 @@ func TestRestRequestErrorHandling(t *testing.T) { t.Errorf("Expected error for non-existent domain, but got nil") } - if !strings.Contains(err.Error(), "HTTP request failed: connection error, timeout, or DNS resolution failure") { + if !strings.Contains(err.Error(), "HTTP request failed: connection error or timeout") { t.Errorf("Expected error message to contain connection failure information, got: %v", err) } @@ -411,11 +411,36 @@ func TestRestRequestErrorHandling(t *testing.T) { t.Errorf("Expected error for 404 status code, but got nil") } - if !strings.Contains(err.Error(), "Unexpected status code: 404") { + if !strings.Contains(err.Error(), "unexpected HTTP status code: 404") { t.Errorf("Expected error message to contain status code 404, got: %v", err) } if step.Success { t.Errorf("Expected step.Success to be false for 404 response") } + + // Test 500 Server Error + ts500 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) // 500 + })) + defer ts500.Close() + + node500 := &avsproto.RestAPINode{ + Url: ts500.URL, + Method: "GET", + } + + step, err = n.Execute("error-test", node500) + + if err == nil { + t.Errorf("Expected error for 500 status code, but got nil") + } + + if !strings.Contains(err.Error(), "unexpected HTTP status code: 500") { + t.Errorf("Expected error message to contain status code 500, got: %v", err) + } + + if step.Success { + t.Errorf("Expected step.Success to be false for 500 response") + } } From 1ada19c84889c027c4f02ee138cc592bda3e5a5d Mon Sep 17 00:00:00 2001 From: chrisli30 Date: Sat, 5 Apr 2025 00:14:51 -0700 Subject: [PATCH 11/11] Revert the capitalization of Errorf messages --- core/taskengine/vm_runner_rest_test.go | 68 +++++++++++++------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/core/taskengine/vm_runner_rest_test.go b/core/taskengine/vm_runner_rest_test.go index 86877efb..568dce2a 100644 --- a/core/taskengine/vm_runner_rest_test.go +++ b/core/taskengine/vm_runner_rest_test.go @@ -60,34 +60,34 @@ func TestRestRequest(t *testing.T) { step, err := n.Execute("123abc", node) if err != nil { - t.Errorf("Expected rest node run successful but got error: %v", err) + t.Errorf("expected rest node run successful but got error: %v", err) } if !step.Success { - t.Errorf("Expected rest node run successfully but failed") + t.Errorf("expected rest node run successfully but failed") } if !strings.Contains(step.Log, "Execute POST httpbin.org at") { - t.Errorf("Expected log to contain request trace data but found no") + t.Errorf("expected log to contain request trace data but found no") } if step.Error != "" { - t.Errorf("Expected log to contain request trace data but found no") + t.Errorf("expected log to contain request trace data but found no") } outputData := gow.AnyToMap(step.GetRestApi().Data)["form"].(map[string]any) //[chat_id:123 disable_notification:true text:*This is a test format*] if outputData["chat_id"].(string) != "123" { - t.Errorf("Expected chat_id to be 123 but got: %s", outputData["chat_id"]) + t.Errorf("expected chat_id to be 123 but got: %s", outputData["chat_id"]) } if outputData["text"].(string) != "*This is a test format*" { - t.Errorf("Expected text to be *This is a test format* but got: %s", outputData["text"]) + t.Errorf("expected text to be *This is a test format* but got: %s", outputData["text"]) } if outputData["disable_notification"].(string) != "true" { - t.Errorf("Expected notification to be disabled but got: %s", outputData["disable_notification"]) + t.Errorf("expected notification to be disabled but got: %s", outputData["disable_notification"]) } } @@ -95,7 +95,7 @@ func TestRestRequestHandleEmptyResponse(t *testing.T) { // Create test server ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - t.Errorf("Expected POST request, got %s", r.Method) + t.Errorf("expected POST request, got %s", r.Method) } w.Header().Set("Content-Type", "application/json") w.Write([]byte("")) @@ -147,15 +147,15 @@ func TestRestRequestHandleEmptyResponse(t *testing.T) { step, err := n.Execute("123abc", node) if err != nil { - t.Errorf("Expected rest node run successful but got error: %v", err) + t.Errorf("expected rest node run successful but got error: %v", err) } if !step.Success { - t.Errorf("Expected rest node run successfully but failed") + t.Errorf("expected rest node run successfully but failed") } if gow.AnyToString(step.GetRestApi().Data) != "" { - t.Errorf("Expected an empty response, got: %s", step.OutputData) + t.Errorf("expected an empty response, got: %s", step.OutputData) } } @@ -163,7 +163,7 @@ func TestRestRequestRenderVars(t *testing.T) { // Create test server ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - t.Errorf("Expected POST request, got %s", r.Method) + t.Errorf("expected POST request, got %s", r.Method) } w.Header().Set("Content-Type", "application/json") body, _ := io.ReadAll(r.Body) @@ -222,15 +222,15 @@ func TestRestRequestRenderVars(t *testing.T) { step, err := n.Execute("123abc", node) if err != nil { - t.Errorf("Expected rest node run successful but got error: %v", err) + t.Errorf("expected rest node run successful but got error: %v", err) } if !step.Success { - t.Errorf("Expected rest node run successfully but failed") + t.Errorf("expected rest node run successfully but failed") } if gow.AnyToString(step.GetRestApi().Data) != "my name is unit test" { - t.Errorf("Expected response to be 'my name is unit test', got: %s", step.OutputData) + t.Errorf("expected response to be 'my name is unit test', got: %s", step.OutputData) } } @@ -238,7 +238,7 @@ func TestRestRequestRenderVarsMultipleExecutions(t *testing.T) { // Create test server ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - t.Errorf("Expected POST request, got %s", r.Method) + t.Errorf("expected POST request, got %s", r.Method) } w.Header().Set("Content-Type", "application/json") body, _ := io.ReadAll(r.Body) @@ -302,13 +302,13 @@ func TestRestRequestRenderVarsMultipleExecutions(t *testing.T) { step, err := n.Execute("123abc", node) if err != nil { - t.Errorf("Expected rest node run successful but got error: %v", err) + t.Errorf("expected rest node run successful but got error: %v", err) } if !step.Success { - t.Errorf("Expected rest node run successfully but failed") + t.Errorf("expected rest node run successfully but failed") } if gow.AnyToString(step.GetRestApi().Data) != "my name is first" { - t.Errorf("Expected response to be 'my name is first', got: %s", step.OutputData) + t.Errorf("expected response to be 'my name is first', got: %s", step.OutputData) } // Second execution with different value @@ -321,24 +321,24 @@ func TestRestRequestRenderVarsMultipleExecutions(t *testing.T) { step, err = n.Execute("123abc", node) if err != nil { - t.Errorf("Expected rest node run successful but got error: %v", err) + t.Errorf("expected rest node run successful but got error: %v", err) } if !step.Success { - t.Errorf("Expected rest node run successfully but failed") + t.Errorf("expected rest node run successfully but failed") } if gow.AnyToString(step.GetRestApi().Data) != "my name is second" { - t.Errorf("Expected response to be 'my name is second', got: %s", step.OutputData) + t.Errorf("expected response to be 'my name is second', got: %s", step.OutputData) } // Verify original node values remain unchanged if node.Url != originalUrl { - t.Errorf("Expected URL to be %s, got %s", originalUrl, node.Url) + t.Errorf("expected URL to be %s, got %s", originalUrl, node.Url) } if node.Body != originalBody { - t.Errorf("Expected Body to be %s, got %s", originalBody, node.Body) + t.Errorf("expected Body to be %s, got %s", originalBody, node.Body) } if !reflect.DeepEqual(node.Headers, originalHeaders) { - t.Errorf("Expected Headers to be %v, got %v", originalHeaders, node.Headers) + t.Errorf("expected Headers to be %v, got %v", originalHeaders, node.Headers) } } @@ -384,15 +384,15 @@ func TestRestRequestErrorHandling(t *testing.T) { step, err := n.Execute("error-test", node) if err == nil { - t.Errorf("Expected error for non-existent domain, but got nil") + t.Errorf("expected error for non-existent domain, but got nil") } if !strings.Contains(err.Error(), "HTTP request failed: connection error or timeout") { - t.Errorf("Expected error message to contain connection failure information, got: %v", err) + t.Errorf("expected error message to contain connection failure information, got: %v", err) } if step.Success { - t.Errorf("Expected step.Success to be false for failed request") + t.Errorf("expected step.Success to be false for failed request") } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -408,15 +408,15 @@ func TestRestRequestErrorHandling(t *testing.T) { step, err = n.Execute("error-test", node404) if err == nil { - t.Errorf("Expected error for 404 status code, but got nil") + t.Errorf("expected error for 404 status code, but got nil") } if !strings.Contains(err.Error(), "unexpected HTTP status code: 404") { - t.Errorf("Expected error message to contain status code 404, got: %v", err) + t.Errorf("expected error message to contain status code 404, got: %v", err) } if step.Success { - t.Errorf("Expected step.Success to be false for 404 response") + t.Errorf("expected step.Success to be false for 404 response") } // Test 500 Server Error @@ -433,14 +433,14 @@ func TestRestRequestErrorHandling(t *testing.T) { step, err = n.Execute("error-test", node500) if err == nil { - t.Errorf("Expected error for 500 status code, but got nil") + t.Errorf("expected error for 500 status code, but got nil") } if !strings.Contains(err.Error(), "unexpected HTTP status code: 500") { - t.Errorf("Expected error message to contain status code 500, got: %v", err) + t.Errorf("expected error message to contain status code 500, got: %v", err) } if step.Success { - t.Errorf("Expected step.Success to be false for 500 response") + t.Errorf("expected step.Success to be false for 500 response") } }