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)