diff --git a/README.md b/README.md index b2684252..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 @@ -110,10 +107,52 @@ 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 + ``` + +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: + + 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 @@ -136,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 @@ -163,6 +201,7 @@ Install the Foundry toolchain with the following commands: curl -L https://foundry.paradigm.xyz | bash foundryup ``` + ### Protobuf Compiler ``` @@ -170,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" @@ -184,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 @@ -207,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/engine.go b/core/taskengine/engine.go index cc104476..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), 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_contract_write.go b/core/taskengine/vm_runner_contract_write.go index 9389c93e..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" @@ -77,11 +80,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 +100,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 +115,63 @@ 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()) + } + + // 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), + Type: uint32(txReceipt.Type), + 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 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..abd45e72 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, @@ -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 } diff --git a/core/taskengine/vm_runner_rest.go b/core/taskengine/vm_runner_rest.go index 4e259d1d..3de0f2f2 100644 --- a/core/taskengine/vm_runner_rest.go +++ b/core/taskengine/vm_runner_rest.go @@ -46,7 +46,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, } @@ -78,6 +78,7 @@ 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 @@ -142,15 +143,25 @@ func (r *RestProcessor) Execute(stepID string, node *avsproto.RestAPINode) (*avs } 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 + // 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 { - s.Success = false - s.Error = fmt.Sprintf("unexpected status code: %d", resp.StatusCode()) - return s, fmt.Errorf("unexpected status code: %d", resp.StatusCode()) + err = fmt.Errorf("unexpected HTTP status code: %d", resp.StatusCode()) + s.Error = err.Error() + return s, err } } diff --git a/core/taskengine/vm_runner_rest_test.go b/core/taskengine/vm_runner_rest_test.go index d7e2bea9..568dce2a 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, @@ -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"]) } } @@ -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, @@ -147,11 +147,11 @@ 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) != "" { @@ -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, @@ -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) } } @@ -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, @@ -308,7 +308,7 @@ func TestRestRequestRenderVarsMultipleExecutions(t *testing.T) { 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 @@ -327,17 +327,120 @@ func TestRestRequestRenderVarsMultipleExecutions(t *testing.T) { 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) + } +} + +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{ + 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 or timeout") { + 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 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") } } 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 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..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 is exist and a valid yaml file %w.", configPath, err)) + 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)