├── .dockerignore
├── .github
├── RELEASE-TEMPLATE.md
└── workflows
│ ├── deploy-avs.yml
│ ├── publish-dev-docker.yml
│ ├── publish-prod-docker.yml
│ ├── release-on-pr-close.yml
│ └── run-test-on-pr.yml
├── .gitignore
├── .gitmodules
├── .golangci.yml
├── .pre-commit-config.yaml
├── Makefile
├── README.md
├── RELEASE.md
├── aggregator
├── aggregator.go
├── auth.go
├── auth_test.go
├── chain_id.go
├── errors.go
├── http_server.go
├── key.go
├── pool.go
├── pool_test.go
├── repl.go
├── resources
│ └── index.gohtml
├── rpc_server.go
├── task_engine.go
└── types
│ └── types.go
├── cmd
├── createAdminKey.go
├── createAliasKey.go
├── declareAlias.go
├── deregister.go
├── register.go
├── removeAlias.go
├── root.go
├── runAggregator.go
├── runOperator.go
├── status.go
└── version.go
├── config
└── operator_sample.yaml
├── contracts
├── .gitignore
├── README.md
├── bindings
│ ├── AutomationServiceManager
│ │ └── binding.go
│ └── AutomationTaskManager
│ │ └── binding.go
├── deploy-ap-config.sh
├── deploy-avs.sh
├── deploy-new-avs-impl.sh
├── deploy-pause-registry.sh
├── deploy-task-manager-impl.sh
├── foundry.toml
├── generate-go-bindings.sh
├── script
│ ├── DeployAPConfig.s.sol
│ ├── DeployPauserRegistry.s.sol
│ ├── DeployServiceManager.s.sol
│ ├── DeployServiceManagerHolesky.s.sol
│ ├── DeployServiceManagerImpl.s.sol
│ ├── DeployTaskManagerImpl.s.sol
│ └── UpgradeServiceManager.s.sol
├── src
│ ├── core
│ │ ├── APConfig.sol
│ │ ├── AutomationServiceManager.sol
│ │ ├── AutomationServiceManagerStorage.sol
│ │ ├── AutomationTaskManager.sol
│ │ └── ERC20Mock.sol
│ └── interfaces
│ │ ├── IAPConfig.sol
│ │ ├── IAutomationServiceManager.sol
│ │ └── IAutomationTaskManager.sol
└── upgrade-avs-impl.sh
├── core
├── apqueue
│ ├── cleanup.go
│ ├── doc.go
│ ├── job.go
│ ├── queue.go
│ └── worker.go
├── auth
│ ├── errors.go
│ ├── operator.go
│ ├── protocol.go
│ ├── server.go
│ ├── telegram.go
│ └── user.go
├── backup
│ ├── backup.go
│ └── backup_test.go
├── chainio
│ ├── aa
│ │ ├── Makefile
│ │ ├── aa.go
│ │ ├── entrypoint.go
│ │ ├── paymaster
│ │ │ └── paymaster.go
│ │ ├── simpleaccount
│ │ │ └── simpleaccount.go
│ │ └── simplefactory.go
│ ├── abis
│ │ ├── account.abi
│ │ ├── apconfig.abi
│ │ ├── entrypoint.abi
│ │ ├── factory.abi
│ │ └── paymaster.abi
│ ├── apconfig
│ │ ├── Makefile
│ │ ├── apconfig.go
│ │ └── binding.go
│ ├── avs_reader.go
│ ├── avs_writer.go
│ ├── bindings.go
│ └── signer
│ │ ├── bls.go
│ │ └── signer.go
├── config
│ ├── config.go
│ ├── envs.go
│ └── helpers.go
├── migrator
│ ├── migrator.go
│ └── migrator_test.go
├── taskengine
│ ├── blockchain_constants.go
│ ├── blockchain_constants_test.go
│ ├── cursor.go
│ ├── custom_code_language_test.go
│ ├── doc.go
│ ├── engine.go
│ ├── engine_crud_test.go
│ ├── engine_execution_test.go
│ ├── engine_pagination_test.go
│ ├── engine_trigger_output_test.go
│ ├── errors.go
│ ├── eth_transfer_integration_test.go
│ ├── event_trigger_test.go
│ ├── executor.go
│ ├── executor_nil_reason_test.go
│ ├── executor_test.go
│ ├── macros
│ │ ├── abi_const.go
│ │ ├── abis
│ │ │ └── chainlink
│ │ │ │ └── eac_aggregator_proxy.json
│ │ ├── contract.go
│ │ ├── contract_test.go
│ │ ├── exp.go
│ │ ├── exp_test.go
│ │ ├── goja_utils.go
│ │ └── goja_utils_test.go
│ ├── modules
│ │ ├── builtins.go
│ │ ├── download-libs.sh
│ │ ├── libs
│ │ │ ├── README.md
│ │ │ ├── dayjs.min.js
│ │ │ ├── lodash.min.js
│ │ │ ├── uuid.js
│ │ │ └── uuid.min.js
│ │ └── registry.go
│ ├── node_types.go
│ ├── operator_notification_test.go
│ ├── pagination.go
│ ├── pagination_empty_test.go
│ ├── pagination_test.go
│ ├── rest_response_processor.go
│ ├── run_node_immediately.go
│ ├── run_node_integration_test.go
│ ├── run_node_templates_test.go
│ ├── run_node_triggers_test.go
│ ├── schema.go
│ ├── secret.go
│ ├── secret_test.go
│ ├── smart_variable_resolution_test.go
│ ├── stats.go
│ ├── stats_test.go
│ ├── task_field_control_test.go
│ ├── token_enrichment_integration_test.go
│ ├── token_metadata.go
│ ├── token_metadata_engine_test.go
│ ├── token_metadata_rpc_test.go
│ ├── token_metadata_service_test.go
│ ├── token_metadata_test.go
│ ├── trigger
│ │ ├── block.go
│ │ ├── block_test.go
│ │ ├── common.go
│ │ ├── event.go
│ │ └── time.go
│ ├── trigger_helper.go
│ ├── utils.go
│ ├── validation.go
│ ├── validation_test.go
│ ├── vm.go
│ ├── vm_branch_execution_test.go
│ ├── vm_contract_operations_test.go
│ ├── vm_event_processing_test.go
│ ├── vm_execution_flow_test.go
│ ├── vm_node_runners_test.go
│ ├── vm_preprocessing_test.go
│ ├── vm_runner_branch.go
│ ├── vm_runner_branch_preprocessing_test.go
│ ├── vm_runner_branch_test.go
│ ├── vm_runner_contract_read.go
│ ├── vm_runner_contract_read_test.go
│ ├── vm_runner_contract_write.go
│ ├── vm_runner_contract_write_test.go
│ ├── vm_runner_contract_write_transaction_limit_test.go
│ ├── vm_runner_customcode.go
│ ├── vm_runner_customcode_modules_test.go
│ ├── vm_runner_customcode_serialization_test.go
│ ├── vm_runner_customcode_test.go
│ ├── vm_runner_eth_transfer.go
│ ├── vm_runner_eth_transfer_test.go
│ ├── vm_runner_filter.go
│ ├── vm_runner_filter_preprocessing_test.go
│ ├── vm_runner_filter_test.go
│ ├── vm_runner_graphql_query.go
│ ├── vm_runner_graphql_query_test.go
│ ├── vm_runner_loop.go
│ ├── vm_runner_loop_test.go
│ ├── vm_runner_rest.go
│ ├── vm_runner_rest_test.go
│ ├── vm_secrets_test.go
│ ├── vm_test_organization.go
│ ├── wallet_test.go
│ └── whitelist.go
├── testutil
│ └── utils.go
└── utils.go
├── docker-compose-test.yml
├── docker-compose.yaml
├── dockerfiles
├── aggregator.Dockerfile
└── operator.Dockerfile
├── docs
├── EventTrigger-Chain-Specific-Search.md
├── EventTrigger-Debug-Summary.md
├── Queue_Cleanup_Implementation.md
├── Smart-Block-Monitoring-Implementation.md
├── Write_Contract_Spec.md
├── contract.md
├── development.md
├── highlevel-diagram.png
├── operator.md
├── protocol.md
└── secret_field_control.md
├── examples
├── README.md
├── decode-create-account.js
├── encode-create-account.js
├── example.js
├── flexible_field_control.go
├── pack.js
├── package-lock.json
├── package.json
├── static_codegen
│ ├── avs_grpc_pb.js
│ └── avs_pb.js
└── util.js
├── fix_all_trigger_reasons.py
├── fix_trigger_reason.py
├── fix_trigger_reason.sh
├── go.mod
├── go.sum
├── main.go
├── metrics
├── metrics.go
└── wrapper.go
├── migrations
├── 20250405-232000-change-epoch-to-ms.go
├── 20250405-232000-change-epoch-to-ms_test.go
├── 20250603-183034-requiredfield-datastructure.go
├── README.md
└── migrations.go
├── model
├── secret.go
├── task.go
├── task_test.go
└── user.go
├── operator
├── alias.go
├── envs.go
├── operator.go
├── password.go
├── process_message.go
├── registration.go
├── worker_loop.go
└── worker_loop_test.go
├── pkg
├── byte4
│ ├── signature.go
│ └── signature_test.go
├── eip1559
│ └── eip1559.go
├── erc20
│ ├── Makefile
│ ├── erc20.abi
│ └── erc20.go
├── erc4337
│ ├── bundler
│ │ ├── client.go
│ │ ├── gas.go
│ │ └── userop.go
│ ├── preset
│ │ ├── builder.go
│ │ └── builder_test.go
│ └── userop
│ │ ├── object.go
│ │ └── parse.go
├── gow
│ └── any.go
├── graphql
│ ├── graphql.go
│ └── graphql_test.go
├── ipfetcher
│ └── ipfetcher.go
└── timekeeper
│ ├── timekeeper.go
│ └── timekeeper_test.go
├── protobuf
├── avs.pb.go
├── avs.proto
├── avs_grpc.pb.go
├── export.go
├── node.pb.go
├── node.proto
└── node_grpc.pb.go
├── scripts
├── compare_storage_structure.go
└── migration
│ └── create_migration.go
├── storage
└── db.go
├── token_whitelist
├── base-sepolia.json
├── base.json
├── ethereum.json
└── sepolia.json
└── version
└── version.go
/.dockerignore:
--------------------------------------------------------------------------------
1 | config/*
2 | dockerfiles/*
3 | .env*
4 |
--------------------------------------------------------------------------------
/.github/RELEASE-TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # Announcements
2 | * First announcement
3 |
4 | # Changes
5 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-avs.yml:
--------------------------------------------------------------------------------
1 | name: Manual AVS Deploy
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | environment:
7 | description: 'Target environment for deployment'
8 | required: true
9 | type: choice
10 | options:
11 | - ethereum
12 | - sepolia
13 | - base
14 | - base-sepolia
15 |
16 | jobs:
17 | aggregator:
18 | runs-on: ubuntu-latest
19 | environment:
20 | name: ${{ inputs.environment }}
21 |
22 | steps:
23 | - name: Checkout code
24 | uses: actions/checkout@v2
25 |
26 | - name: SSH and Deploy
27 | uses: appleboy/ssh-action@v0.1.5
28 | with:
29 | host: ${{ secrets.AVS_SERVER_HOST }}
30 | username: ava
31 | key: ${{ secrets.AVS_SSH_KEY }}
32 | script: |
33 | echo "Deploying to environment: ${{ inputs.environment }}"
34 | cd $HOME/ap-aggregator-setup/${{ inputs.environment }}
35 | docker compose pull
36 | docker compose up -d --force-recreate
37 |
38 | operator:
39 | runs-on: ubuntu-latest
40 | environment:
41 | name: ${{ inputs.environment }}
42 |
43 | steps:
44 | - name: Checkout code
45 | uses: actions/checkout@v2
46 |
47 | - name: SSH and Deploy
48 | uses: appleboy/ssh-action@v0.1.5
49 | with:
50 | host: ${{ secrets.AVS_SERVER_HOST }}
51 | username: ava
52 | key: ${{ secrets.AVS_SSH_KEY }}
53 | script: |
54 | echo "Deploying to environment: ${{ inputs.environment }}"
55 | cd $HOME/ap-operator-setup/${{ inputs.environment == 'sepolia' && 'holesky' || inputs.environment }}
56 | docker compose pull
57 | docker compose up -d --force-recreate
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiler files
2 | cache/
3 | out/
4 |
5 | # Ignores development broadcast logs
6 | !/broadcast
7 | /broadcast/*/31337/
8 | /broadcast/**/dry-run/
9 |
10 | # Dotenv file
11 | .env
12 | *.env
13 | .env*
14 |
15 | # binary
16 | ap-avs
17 |
18 | .DS_Store
19 | node_modules
20 |
21 | # Ignores operator configuration files at root
22 | operator.yaml
23 | metadata.json
24 |
25 | # Ignores yaml configuration files in the config directory
26 | config/*.yaml
27 |
28 | contracts/script/output*
29 |
30 | contracts/broadcast/
31 | alias-ecdsa.key.json
32 |
33 | keys/
34 |
35 | # Plugin files for semantic release
36 | .semrel/
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "contracts/lib/forge-std"]
2 | path = contracts/lib/forge-std
3 | url = https://github.com/foundry-rs/forge-std
4 | [submodule "contracts/lib/eigenlayer-middleware"]
5 | path = contracts/lib/eigenlayer-middleware
6 | url = https://github.com/Layr-Labs/eigenlayer-middleware
7 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | # Options for analysis running.
2 | run:
3 | # Default concurrency is a available CPU number.
4 | # Concurrency: 4
5 |
6 | # Timeout for analysis, e.g. 30s, 5m.
7 | # Default: 1m
8 | timeout: 5m
9 |
10 | # Go version to use for analysis
11 | # Default: auto-detected
12 | go: "1.22"
13 |
14 | # Exit code when at least one issue was found.
15 | # Default: 1
16 | issues-exit-code: 1
17 |
18 | # Include test files or not.
19 | # Default: true
20 | tests: true
21 |
22 | # List of build tags, all linters use it.
23 | # Default: []
24 | build-tags:
25 | - integration
26 |
27 | # Which dirs to skip: issues from them won't be reported.
28 | # Default value is empty list, but there is no need to include all autogenerated files.
29 | skip-dirs:
30 | - vendor
31 |
32 | # Which files to skip: they will be analyzed, but issues from them won't be reported.
33 | # Default value is empty list, but there is no need to include all autogenerated files.
34 | skip-files:
35 | - ".*\\.pb\\.go$"
36 |
37 | # Output configuration options
38 | output:
39 | # Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
40 | # Default: colored-line-number
41 | format: colored-line-number
42 |
43 | # Print lines of code with issue.
44 | # Default: true
45 | print-issued-lines: true
46 |
47 | # Print linter name in the end of issue text.
48 | # Default: true
49 | print-linter-name: true
50 |
51 | # All available settings of specific linters.
52 | linters-settings:
53 | errcheck:
54 | # Report about not checking of errors in type assertions: `a := b.(MyStruct)`.
55 | # Default: false
56 | check-type-assertions: true
57 |
58 | gocyclo:
59 | # Minimal code complexity to report.
60 | # Default: 30
61 | min-complexity: 15
62 |
63 | gofmt:
64 | # Simplify code: gofmt with `-s` option.
65 | # Default: true
66 | simplify: true
67 |
68 | govet:
69 | # Report about shadowed variables.
70 | # Default: false
71 | check-shadowing: true
72 |
73 | revive:
74 | # Minimal confidence for issues.
75 | # Default: 0.8
76 | min-confidence: 0.8
77 |
78 | unused:
79 | # Treat code as a program (not a library) and report unused exported identifiers.
80 | # Default: false
81 | check-exported: false
82 |
83 | linters:
84 | disable-all: true
85 | enable:
86 | - errcheck
87 | - gosimple
88 | - govet
89 | - ineffassign
90 | - staticcheck
91 | - typecheck
92 | - unused
93 | - gocyclo
94 | - gofmt
95 | - goimports
96 | - misspell
97 | - revive
98 | - unconvert
99 | - unparam
100 | - whitespace
101 |
102 | issues:
103 | # Maximum count of issues with the same text.
104 | # Default: 3
105 | max-same-issues: 5
106 |
107 | # Maximum issues count per one linter.
108 | # Default: 50
109 | max-issues-per-linter: 50
110 |
111 | # List of regexps of issue texts to exclude.
112 | exclude-rules:
113 | # Exclude some linters from running on tests files.
114 | - path: _test\.go
115 | linters:
116 | - gocyclo
117 | - errcheck
118 | - dupl
119 | - gosec
120 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: local
3 | hooks:
4 | - id: go-fmt
5 | name: go fmt
6 | entry: go fmt ./...
7 | language: system
8 | types: [go]
9 | pass_filenames: false
10 |
11 | - id: go-mod-tidy
12 | name: go mod tidy
13 | entry: go mod tidy
14 | language: system
15 | types: [go]
16 | pass_filenames: false
17 |
18 | - id: go-vet
19 | name: go vet
20 | entry: go vet ./...
21 | language: system
22 | types: [go]
23 | pass_filenames: false
24 |
25 | # - id: golangci-lint
26 | # name: golangci-lint
27 | # entry: golangci-lint run
28 | # language: system
29 | # types: [go]
30 | # pass_filenames: false
31 |
32 | - id: conventional-commit-check
33 | name: Conventional Commit Message Check
34 | entry: |-
35 | bash -c '
36 | COMMIT_MSG_FILE="$1"
37 | FIRST_LINE=$(head -n1 "$COMMIT_MSG_FILE")
38 | # Regex for conventional commit: type(optional_scope)!: subject
39 | # Allowed types: feat, fix, docs, style, refactor, perf, test, chore, ci, build, revert
40 | # Allows an optional scope in parentheses e.g. feat(parser):
41 | # Allows an optional ! for breaking change e.g. feat!:
42 | # Requires a colon and a space after type/scope/!.
43 | # Requires some subject text.
44 | PATTERN="^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\([\w\s-]+\))?!?:\s.+"
45 | if ! grep -Eq "$PATTERN" <<< "$FIRST_LINE"; then
46 | echo "-------------------------------------------------------------------"
47 | echo "ERROR: Commit message does not follow Conventional Commits format."
48 | echo "-------------------------------------------------------------------"
49 | echo "It must start with a type like feat, fix, docs, etc.,"
50 | echo "followed by an optional scope in parentheses (e.g. (api), (ui)),"
51 | echo "an optional exclamation mark for breaking changes (!),"
52 | echo "a colon and a single space, and then the subject."
53 | echo ""
54 | echo "Examples:"
55 | echo " feat: add new user authentication feature"
56 | echo " fix(parser): correctly handle empty input"
57 | echo " docs!: update API documentation for breaking change"
58 | echo ""
59 | echo "Allowed types: feat, fix, docs, style, refactor, perf, test, chore, ci, build, revert"
60 | echo "For more details, see: https://www.conventionalcommits.org/"
61 | echo "-------------------------------------------------------------------"
62 | exit 1
63 | fi
64 | '
65 | language: system
66 | stages: [commit-msg]
67 | pass_filenames: true # The script expects the filename as $1
68 |
--------------------------------------------------------------------------------
/aggregator/chain_id.go:
--------------------------------------------------------------------------------
1 | package aggregator
2 |
3 | import (
4 | "math/big"
5 | "sync"
6 | )
7 |
8 | var (
9 | globalChainID *big.Int
10 | globalChainIDLock sync.RWMutex
11 | )
12 |
13 | func SetGlobalChainID(chainID *big.Int) {
14 | globalChainIDLock.Lock()
15 | defer globalChainIDLock.Unlock()
16 | globalChainID = chainID
17 | }
18 |
19 | func GetGlobalChainID() *big.Int {
20 | globalChainIDLock.RLock()
21 | defer globalChainIDLock.RUnlock()
22 | return globalChainID
23 | }
24 |
--------------------------------------------------------------------------------
/aggregator/errors.go:
--------------------------------------------------------------------------------
1 | package aggregator
2 |
3 | const (
4 | InternalError = "Internal Error"
5 | )
6 |
--------------------------------------------------------------------------------
/aggregator/http_server.go:
--------------------------------------------------------------------------------
1 | package aggregator
2 |
3 | import (
4 | "bytes"
5 | "embed"
6 | "fmt"
7 | "text/template"
8 |
9 | "context"
10 | "net/http"
11 |
12 | "github.com/AvaProtocol/EigenLayer-AVS/version"
13 | "github.com/getsentry/sentry-go"
14 | sentryecho "github.com/getsentry/sentry-go/echo"
15 | "github.com/labstack/echo/v4"
16 | "github.com/labstack/echo/v4/middleware"
17 | )
18 |
19 | var (
20 | //go:embed resources
21 | res embed.FS
22 | )
23 |
24 | type HttpJsonResp[T any] struct {
25 | Data T `json:"data"`
26 | }
27 |
28 | func (agg *Aggregator) startHttpServer(ctx context.Context) {
29 | sentryDsn := ""
30 |
31 | if agg.config != nil {
32 | sentryDsn = agg.config.SentryDsn
33 | }
34 |
35 | fmt.Printf("Sentry DSN from config: %s\n", sentryDsn)
36 |
37 | if sentryDsn != "" {
38 | serverName := ""
39 | if agg.config != nil {
40 | serverName = agg.config.ServerName
41 | }
42 |
43 | fmt.Printf("Sentry ServerName from config: %s\n", serverName)
44 |
45 | // To initialize Sentry's handler, you need to initialize Sentry itself beforehand
46 | if err := sentry.Init(sentry.ClientOptions{
47 | Dsn: sentryDsn,
48 | ServerName: serverName,
49 | // Set TracesSampleRate to 1.0 to capture 100%
50 | // of transactions for tracing.
51 | // We recommend adjusting this value in production.
52 | TracesSampleRate: 1.0,
53 | // Adds request headers and IP for users,
54 | // visit: https://docs.sentry.io/platforms/go/data-management/data-collected/ for more info
55 | SendDefaultPII: true,
56 | }); err != nil {
57 | agg.logger.Errorf("Sentry initialization failed: %v", err)
58 | }
59 | }
60 |
61 | e := echo.New()
62 |
63 | e.Use(middleware.Logger())
64 | e.Use(middleware.Recover())
65 |
66 | // Once it's done, you can attach the handler as one of your middleware
67 | // Only add Sentry middleware if DSN was provided and Sentry initialized
68 | if sentryDsn != "" {
69 | e.Use(sentryecho.New(sentryecho.Options{
70 | Repanic: true,
71 | WaitForDelivery: false,
72 | }))
73 | }
74 |
75 | e.GET("/up", func(c echo.Context) error {
76 | if agg.status == runningStatus {
77 | return c.String(http.StatusOK, "up")
78 | }
79 |
80 | return c.String(http.StatusServiceUnavailable, "pending...")
81 | })
82 |
83 | e.GET("/operator", func(c echo.Context) error {
84 | return c.JSON(http.StatusOK, &HttpJsonResp[[]*OperatorNode]{
85 | Data: agg.operatorPool.GetAll(),
86 | })
87 | })
88 |
89 | e.GET("/telemetry", func(c echo.Context) error {
90 | tpl, err := template.ParseFS(res, "resources/*.gohtml")
91 |
92 | if err != nil {
93 | agg.logger.Errorf("error rendering telemetry %v", err)
94 | return err
95 | }
96 |
97 | data := struct {
98 | Version string
99 | Revision string
100 | Nodes []*OperatorNode
101 | }{
102 | Version: version.Get(),
103 | Revision: version.Commit(),
104 | Nodes: agg.operatorPool.GetAll(),
105 | }
106 | var buf bytes.Buffer
107 | if err := tpl.Execute(&buf, data); err != nil {
108 | agg.logger.Errorf("error rendering telemetry %v", err)
109 | return err
110 | }
111 |
112 | return c.HTMLBlob(http.StatusOK, buf.Bytes())
113 | })
114 |
115 | go func() {
116 | e.Logger.Fatal(e.Start(":1323"))
117 | }()
118 | }
119 |
--------------------------------------------------------------------------------
/aggregator/key.go:
--------------------------------------------------------------------------------
1 | package aggregator
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/AvaProtocol/EigenLayer-AVS/core/auth"
8 | "github.com/AvaProtocol/EigenLayer-AVS/core/config"
9 | "github.com/golang-jwt/jwt/v5"
10 | )
11 |
12 | type CreateApiKeyOption struct {
13 | Roles []string
14 | Subject string
15 | }
16 |
17 | // Create an JWT admin key to manage user task
18 | func CreateAdminKey(configPath string, opt CreateApiKeyOption) error {
19 | nodeConfig, err := config.NewConfig(configPath)
20 | if err != nil {
21 | return fmt.Errorf("failed to parse config file: %s\nMake sure it is exist and a valid yaml file %w.", configPath, err)
22 | }
23 |
24 | aggregator, err := NewAggregator(nodeConfig)
25 | if err != nil {
26 | return fmt.Errorf("cannot initialize aggregrator from config: %w", err)
27 | }
28 |
29 | if opt.Subject == "" {
30 | return fmt.Errorf("error: subject cannot be empty")
31 | }
32 |
33 | if len(opt.Roles) < 1 {
34 | return fmt.Errorf("error: at least one role is required")
35 | }
36 |
37 | roles := make([]auth.ApiRole, len(opt.Roles))
38 | for i, v := range opt.Roles {
39 | roles[i] = auth.ApiRole(v)
40 | }
41 |
42 | claims := &auth.APIClaim{
43 | RegisteredClaims: &jwt.RegisteredClaims{
44 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 365 * 10)),
45 | Issuer: "AvaProtocol",
46 | Subject: opt.Subject,
47 | },
48 | Roles: roles,
49 | }
50 |
51 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
52 | ss, err := token.SignedString(aggregator.config.JwtSecret)
53 |
54 | fmt.Println(ss)
55 |
56 | return err
57 | }
58 |
--------------------------------------------------------------------------------
/aggregator/pool.go:
--------------------------------------------------------------------------------
1 | package aggregator
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "time"
8 |
9 | timestamppb "google.golang.org/protobuf/types/known/timestamppb"
10 |
11 | "github.com/AvaProtocol/EigenLayer-AVS/core/config"
12 | avsproto "github.com/AvaProtocol/EigenLayer-AVS/protobuf"
13 | "github.com/AvaProtocol/EigenLayer-AVS/storage"
14 | )
15 |
16 | type OperatorNode struct {
17 | Address string `json:"address"`
18 | RemoteIP string `json:"remote_ip"`
19 | LastPingEpoch int64 `json:"last_ping"`
20 | Version string `json:"version"`
21 | MetricsPort int32 `json:"metrics_port"`
22 | BlockNumer int64 `json:"block_number"`
23 | EventCount int64 `json:"event_count"`
24 | }
25 |
26 | func (o *OperatorNode) LastSeen() string {
27 | now := time.Now()
28 |
29 | var last time.Time
30 | if o.LastPingEpoch > 1e12 { // Threshold for milliseconds (timestamps after 2001)
31 | last = time.Unix(o.LastPingEpoch/1000, 0)
32 | } else {
33 | last = time.Unix(o.LastPingEpoch, 0)
34 | }
35 |
36 | duration := now.Sub(last)
37 |
38 | days := int(duration.Hours()) / 24
39 | hours := int(duration.Hours()) % 24
40 | minutes := int(duration.Minutes()) % 60
41 | seconds := int(duration.Seconds()) % 60
42 |
43 | if days > 0 {
44 | return fmt.Sprintf("%dd%dh ago", days, hours)
45 | } else if hours > 0 {
46 | return fmt.Sprintf("%dh%dm ago", hours, minutes)
47 | } else if minutes > 0 {
48 | return fmt.Sprintf("%dm%ds ago", minutes, seconds)
49 | } else {
50 | return fmt.Sprintf("%ds ago", seconds)
51 | }
52 | }
53 |
54 | func (o *OperatorNode) EtherscanURL() string {
55 | return fmt.Sprintf("%s/address/%s", config.EtherscanURL(), o.Address)
56 | }
57 |
58 | func (o *OperatorNode) EigenlayerURL() string {
59 | return fmt.Sprintf("%s/operator/%s", config.EigenlayerAppURL(), o.Address)
60 | }
61 |
62 | var (
63 | operatorPrefix = []byte("operator:")
64 | )
65 |
66 | type OperatorPool struct {
67 | db storage.Storage
68 | }
69 |
70 | func (o *OperatorPool) Checkin(payload *avsproto.Checkin) error {
71 | now := time.Now()
72 |
73 | status := &OperatorNode{
74 | Address: payload.Address,
75 | LastPingEpoch: now.Unix(),
76 | MetricsPort: payload.MetricsPort,
77 | RemoteIP: payload.RemoteIP,
78 | Version: payload.Version,
79 | BlockNumer: payload.BlockNumber,
80 | EventCount: payload.EventCount,
81 | }
82 |
83 | data, err := json.Marshal(status)
84 |
85 | if err != nil {
86 | return fmt.Errorf("cannot update operator status due to json encoding")
87 | }
88 |
89 | return o.db.Set(append(operatorPrefix, []byte(payload.Id)...), data)
90 | }
91 |
92 | func (o *OperatorPool) GetAll() []*OperatorNode {
93 | var nodes []*OperatorNode
94 |
95 | kvs, err := o.db.GetByPrefix(operatorPrefix)
96 | if err != nil {
97 | return nodes
98 | }
99 |
100 | for _, rawValue := range kvs {
101 | node := &OperatorNode{}
102 | if err := json.Unmarshal(rawValue.Value, node); err != nil {
103 | continue
104 | }
105 |
106 | nodes = append(nodes, node)
107 | }
108 |
109 | return nodes
110 | }
111 |
112 | func (r *RpcServer) Ping(ctx context.Context, payload *avsproto.Checkin) (*avsproto.CheckinResp, error) {
113 | if ok, err := r.verifyOperator(ctx, payload.Address); !ok {
114 | return nil, err
115 | }
116 |
117 | if err := r.operatorPool.Checkin(payload); err != nil {
118 | return nil, fmt.Errorf("cannot update operator status error: %w", err)
119 | }
120 |
121 | return &avsproto.CheckinResp{
122 | UpdatedAt: timestamppb.Now(),
123 | }, nil
124 | }
125 |
--------------------------------------------------------------------------------
/aggregator/pool_test.go:
--------------------------------------------------------------------------------
1 | package aggregator
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | func TestLastSeen(t *testing.T) {
9 | now := time.Now()
10 |
11 | testCases := []struct {
12 | name string
13 | lastPingEpoch int64
14 | expectedFormat string
15 | }{
16 | {
17 | name: "seconds ago (milliseconds)",
18 | lastPingEpoch: now.Add(-10 * time.Second).UnixMilli(),
19 | expectedFormat: "10s ago",
20 | },
21 | {
22 | name: "minutes ago (milliseconds)",
23 | lastPingEpoch: now.Add(-5 * time.Minute).UnixMilli(),
24 | expectedFormat: "5m0s ago",
25 | },
26 | {
27 | name: "hours ago (milliseconds)",
28 | lastPingEpoch: now.Add(-2 * time.Hour).UnixMilli(),
29 | expectedFormat: "2h0m ago",
30 | },
31 | {
32 | name: "days ago (milliseconds)",
33 | lastPingEpoch: now.Add(-48 * time.Hour).UnixMilli(),
34 | expectedFormat: "2d0h ago",
35 | },
36 | {
37 | name: "seconds ago (seconds)",
38 | lastPingEpoch: now.Add(-10 * time.Second).Unix(),
39 | expectedFormat: "10s ago",
40 | },
41 | {
42 | name: "minutes ago (seconds)",
43 | lastPingEpoch: now.Add(-5 * time.Minute).Unix(),
44 | expectedFormat: "5m0s ago",
45 | },
46 | }
47 |
48 | for _, tc := range testCases {
49 | t.Run(tc.name, func(t *testing.T) {
50 | node := &OperatorNode{
51 | Address: "0x123",
52 | RemoteIP: "127.0.0.1",
53 | LastPingEpoch: tc.lastPingEpoch,
54 | Version: "1.0.0",
55 | }
56 |
57 | result := node.LastSeen()
58 | if result != tc.expectedFormat {
59 | t.Errorf("Expected format %s, got %s", tc.expectedFormat, result)
60 | }
61 | })
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/aggregator/resources/index.gohtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Ava Dashboard
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{ range .Nodes }}
13 | -
14 |
15 | {{/* TODO: Parse metadata to get image url

*/}}
18 |
28 |
29 |
30 |
31 |
32 | {{ if ne .Version "" }}
33 |
34 | {{ .Version }}
35 |
36 | {{ end }}
37 |
43 |
44 |
45 |
46 |
47 |
48 |
Active
49 |
Last seen
50 |
Block {{ .BlockNumer }}
51 |
Event {{ .EventCount }}
52 |
53 | {{ end }}
54 |
55 |
56 |
57 |
58 |
59 |
60 | Aggregator v{{.Version}}
61 | +{{.Revision}}
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/aggregator/types/types.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | sdktypes "github.com/Layr-Labs/eigensdk-go/types"
5 | "github.com/ethereum/go-ethereum/common"
6 | )
7 |
8 | // TODO: Hardcoded for now
9 | // all operators in quorum0 must sign the task response in order for it to be accepted
10 | const QUORUM_THRESHOLD_NUMERATOR = uint32(100)
11 | const QUORUM_THRESHOLD_DENOMINATOR = uint32(100)
12 |
13 | const QUERY_FILTER_FROM_BLOCK = uint64(1)
14 |
15 | // we only use a single quorum (quorum 0) for incredible squaring
16 | var QUORUM_NUMBERS = []byte{0}
17 |
18 | type BlockNumber = uint32
19 | type TaskIndex = uint32
20 |
21 | type OperatorInfo struct {
22 | OperatorPubkeys sdktypes.OperatorPubkeys
23 | OperatorAddr common.Address
24 | }
25 |
--------------------------------------------------------------------------------
/cmd/createAdminKey.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/AvaProtocol/EigenLayer-AVS/aggregator"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | var (
10 | apiKeyOption = aggregator.CreateApiKeyOption{}
11 | createApiKey = &cobra.Command{
12 | Use: "create-api-key",
13 | Short: "Create a long live JWT key to interact with userdata of AVS",
14 | Long: `Create an JWT key that allow one to manage user tasks. This key cannot control operator aspect, only user storage such as tasks management`,
15 | Run: func(cmd *cobra.Command, args []string) {
16 | aggregator.CreateAdminKey(config, apiKeyOption)
17 | },
18 | }
19 | )
20 |
21 | func init() {
22 | createApiKey.Flags().StringArrayVar(&(apiKeyOption.Roles), "role", []string{}, "Role for API Key")
23 | createApiKey.Flags().StringVarP(&(apiKeyOption.Subject), "subject", "s", "admin", "subject name to be use for jwt api key")
24 | rootCmd.AddCommand(createApiKey)
25 | }
26 |
--------------------------------------------------------------------------------
/cmd/createAliasKey.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2024 Ava Protocol
3 | */
4 | package cmd
5 |
6 | import (
7 | "github.com/spf13/cobra"
8 |
9 | "github.com/AvaProtocol/EigenLayer-AVS/operator"
10 | )
11 |
12 | var (
13 | aliasKeyOption = operator.CreateAliasKeyOption{}
14 | )
15 |
16 | // createAliasKeyCmd represents the createAliasKey command
17 | var createAliasKeyCmd = &cobra.Command{
18 | Use: "create-alias-key",
19 | Short: "Create an ECDSA private key only for AP AVS operation",
20 | Long: `Generate an ECDSA private key to use for AP AVS operation.
21 |
22 | Instead of using the operator's ECDSA private key to interact with
23 | Ava Protocol AVS, you can generate an alias key and use this key to
24 | interact with Ava Protocol operator. You will still need the EigenLayer
25 | Operator ECDSA key to register or deregister from the AVS. But once
26 | you registered, you don't need that operator key anymore`,
27 | Run: func(cmd *cobra.Command, args []string) {
28 | operator.CreateOrImportAliasKey(aliasKeyOption)
29 | },
30 | }
31 |
32 | func init() {
33 | rootCmd.AddCommand(createAliasKeyCmd)
34 |
35 | createAliasKeyCmd.Flags().StringVarP(&(aliasKeyOption.PrivateKey), "ecdsa-private-key", "k", "", "a private key start with 0x to import as alias key")
36 |
37 | createAliasKeyCmd.Flags().StringVarP(&(aliasKeyOption.Filename), "name", "n", "alias-ecdsa.key.json", "absolute or relative file path to save your ECDSA key to")
38 | createAliasKeyCmd.MarkPersistentFlagRequired("name")
39 | }
40 |
--------------------------------------------------------------------------------
/cmd/declareAlias.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2024 Ava Protocol
3 | */
4 | package cmd
5 |
6 | import (
7 | "github.com/spf13/cobra"
8 |
9 | "github.com/AvaProtocol/EigenLayer-AVS/operator"
10 | )
11 |
12 | // declareAliasCmd represents the declareAlias command
13 | var declareAliasCmd = &cobra.Command{
14 | Use: "declare-alias",
15 | Short: "Declare an alias ecdsa key file for the operator address",
16 | Long: `Declare an alias ecdsa key file for the operator address
17 |
18 | After creating an alias key, they key can be declared as
19 | an alias for the operator address`,
20 | Run: func(cmd *cobra.Command, args []string) {
21 | operator.DeclareAlias(config, aliasKeyOption.Filename)
22 | },
23 | }
24 |
25 | func init() {
26 | rootCmd.AddCommand(declareAliasCmd)
27 |
28 | declareAliasCmd.Flags().StringVarP(&(aliasKeyOption.Filename), "name", "n", "alias-ecdsa.key.json", "absolute or relative file path to alias ECDSA key to declare alias")
29 | declareAliasCmd.MarkPersistentFlagRequired("name")
30 | }
31 |
--------------------------------------------------------------------------------
/cmd/deregister.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2024 Ava Protocol
3 | */
4 | package cmd
5 |
6 | import (
7 | "github.com/spf13/cobra"
8 |
9 | "github.com/AvaProtocol/EigenLayer-AVS/operator"
10 | )
11 |
12 | var (
13 | deRegisterCmd = &cobra.Command{
14 | Use: "deregister",
15 | Short: "DeRegister your operator from Oak AVS",
16 | Long: `Opt-out your operator from AVS.\nThe process will failed if you haven't opt-in yet`,
17 | Run: func(cmd *cobra.Command, args []string) {
18 | operator.DeregisterFromAVS(config)
19 | },
20 | }
21 | )
22 |
23 | func init() {
24 | rootCmd.AddCommand(deRegisterCmd)
25 | }
26 |
--------------------------------------------------------------------------------
/cmd/register.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 |
6 | "github.com/AvaProtocol/EigenLayer-AVS/operator"
7 | )
8 |
9 | var (
10 | registerCmd = &cobra.Command{
11 | Use: "register",
12 | Short: "Register your operator with Oak AVS",
13 | Long: `The registration requires that an operator already register on Eigenlayer
14 | and to have a minimun amount of staked delegated`,
15 | Run: func(cmd *cobra.Command, args []string) {
16 | operator.RegisterToAVS(config)
17 | },
18 | }
19 | )
20 |
21 | func init() {
22 | rootCmd.AddCommand(registerCmd)
23 | }
24 |
--------------------------------------------------------------------------------
/cmd/removeAlias.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2024 Ava Protocol
3 | */
4 | package cmd
5 |
6 | import (
7 | "github.com/spf13/cobra"
8 |
9 | "github.com/AvaProtocol/EigenLayer-AVS/operator"
10 | )
11 |
12 | // removeAliasCmd represents the removeAlias command
13 | var removeAliasCmd = &cobra.Command{
14 | Use: "remove-alias",
15 | Short: "Unbind alias address from your operator",
16 | Long: `Unbind alias key from your operator address
17 |
18 | After removal, you will either need to setup another alias key, or to use your operator ECDSA key.
19 |
20 | When removing alias, you can run it with alias key
21 | `,
22 | Run: func(cmd *cobra.Command, args []string) {
23 | operator.RemoveAlias(config)
24 | },
25 | }
26 |
27 | func init() {
28 | rootCmd.AddCommand(removeAliasCmd)
29 | }
30 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | // rootCmd represents the base command when called without any subcommands
10 | var (
11 | config = "./config/operator.yaml"
12 | rootCmd = &cobra.Command{
13 | Use: "ap-avs",
14 | Short: "OAK AVS CLI",
15 | Long: `OAK CLI to run and interact with EigenLayer service.
16 | Each sub command can be use for a single service
17 |
18 | Such as "ap-avs run-operator" or "ap-avs run-aggregrator" and so on
19 | `,
20 | }
21 | )
22 |
23 | func Execute() {
24 | err := rootCmd.Execute()
25 | if err != nil {
26 | os.Exit(1)
27 | }
28 | }
29 |
30 | func init() {
31 | rootCmd.Flags().BoolP("analytic", "t", false, "send back telemetry to Oak")
32 | rootCmd.PersistentFlags().StringVarP(&config, "config", "c", "./config/operator.yaml", "Path to config file")
33 | }
34 |
--------------------------------------------------------------------------------
/cmd/runAggregator.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/AvaProtocol/EigenLayer-AVS/aggregator"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | var (
10 | runAggregatorCmd = &cobra.Command{
11 | Use: "aggregator",
12 | Short: "Run aggregator",
13 | Long: `Initialize and run aggregator.
14 |
15 | Use --config=path-to-your-config-file. default is=./config/aggregator.yaml `,
16 | Run: func(cmd *cobra.Command, args []string) {
17 | aggregator.RunWithConfig(config)
18 | },
19 | }
20 | )
21 |
22 | func init() {
23 | registerCmd.Flags().StringVar(&config, "config", "./config/aggregator.yaml", "path to aggregrator config file")
24 | rootCmd.AddCommand(runAggregatorCmd)
25 | }
26 |
--------------------------------------------------------------------------------
/cmd/runOperator.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2024 NAME HERE
3 | */
4 | package cmd
5 |
6 | import (
7 | "github.com/spf13/cobra"
8 |
9 | "github.com/AvaProtocol/EigenLayer-AVS/operator"
10 | )
11 |
12 | var (
13 | runOperatorCmd = &cobra.Command{
14 | Use: "operator",
15 | Short: "start operator process",
16 | Long: `use --debug to dump more log.
17 | Make sure to run "operator register" before starting up operator`,
18 | Run: func(cmd *cobra.Command, args []string) {
19 | operator.RunWithConfig(config)
20 | },
21 | }
22 | )
23 |
24 | func init() {
25 | rootCmd.AddCommand(runOperatorCmd)
26 | }
27 |
--------------------------------------------------------------------------------
/cmd/status.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2024 NAME HERE
3 | */
4 | package cmd
5 |
6 | import (
7 | "github.com/spf13/cobra"
8 |
9 | "github.com/AvaProtocol/EigenLayer-AVS/operator"
10 | )
11 |
12 | var (
13 | statusCmd = &cobra.Command{
14 | Use: "status",
15 | Short: "report operator status",
16 | Long: `Report status of your operator with OAK AVS`,
17 | Run: func(cmd *cobra.Command, args []string) {
18 | operator.Status(config)
19 | },
20 | }
21 | )
22 |
23 | func init() {
24 | rootCmd.AddCommand(statusCmd)
25 | }
26 |
--------------------------------------------------------------------------------
/cmd/version.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2024 NAME HERE
3 | */
4 | package cmd
5 |
6 | import (
7 | "fmt"
8 |
9 | "github.com/spf13/cobra"
10 |
11 | "github.com/AvaProtocol/EigenLayer-AVS/version"
12 | )
13 |
14 | // versionCmd represents the version command
15 | var versionCmd = &cobra.Command{
16 | Use: "version",
17 | Short: "get version",
18 | Long: `get version of the binary`,
19 | Run: func(cmd *cobra.Command, args []string) {
20 | fmt.Printf("%s\n", version.Get())
21 | },
22 | }
23 |
24 | func init() {
25 | rootCmd.AddCommand(versionCmd)
26 | }
27 |
--------------------------------------------------------------------------------
/config/operator_sample.yaml:
--------------------------------------------------------------------------------
1 | # this sets the logger level (true = info, false = debug)
2 | production: false
3 |
4 | # Replace with your operator
5 | operator_address: 0x997E5D40a32c44a3D93E59fC55C4Fd20b7d2d49D
6 |
7 | avs_registry_coordinator_address: 0x90c6d6f2A78d5Ce22AB8631Ddb142C03AC87De7a
8 | operator_state_retriever_address: 0xb7bb920538e038DFFEfcB55caBf713652ED2031F
9 |
10 | # Replace with your holesky RPC
11 | eth_rpc_url:
12 | eth_ws_url:
13 |
14 | ecdsa_private_key_store_path:
15 | #ecdsa_private_key_store_path: alias-ecdsa.key.json
16 |
17 | # If you running this using eigenlayer CLI and the provided AVS packaging structure,
18 | # this should be /operator_keys/bls_key.json as the host path will be asked while running
19 | #
20 | # We are using bn254 curve for bls keys
21 | #
22 | # If you are running locally using go run main.go, this should be full path to your local bls key file
23 | bls_private_key_store_path:
24 |
25 | aggregator_server_ip_port_address: "127.0.0.1:2206"
26 |
27 | # avs node spec compliance https://eigen.nethermind.io/docs/spec/intro
28 | eigen_metrics_ip_port_address: localhost:9090
29 | enable_metrics: true
30 | node_api_ip_port_address: localhost:9010
31 | enable_node_api: true
32 |
33 | db_path: /tmp/ap-avs-operator
34 |
35 | backup:
36 | enabled: false
37 | interval_minutes: 60
38 | backup_dir: "./backup"
39 |
40 | # Destination chain where the task run, relace with your actualy target chain
41 | target_chain:
42 | eth_rpc_url:
43 | eth_ws_url:
44 |
45 | enabled_features:
46 | # event trigger requires a dedicated rpc node with websocket to listen to all on-chain event. Depend on your RPC provider, this may require significant billing so we disable by default
47 | event_trigger: false
48 |
--------------------------------------------------------------------------------
/contracts/.gitignore:
--------------------------------------------------------------------------------
1 | cache
2 | out
3 | data
4 |
--------------------------------------------------------------------------------
/contracts/README.md:
--------------------------------------------------------------------------------
1 | ## Foundry
2 |
3 | **Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
4 |
5 | Foundry consists of:
6 |
7 | - **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
8 | - **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
9 | - **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
10 | - **Chisel**: Fast, utilitarian, and verbose solidity REPL.
11 |
12 | ## Documentation
13 |
14 | https://book.getfoundry.sh/
15 |
16 | ## Usage
17 |
18 | ### Build
19 |
20 | ```shell
21 | $ forge build
22 | ```
23 |
24 | ### Test
25 |
26 | ```shell
27 | $ forge test
28 | ```
29 |
30 | ### Format
31 |
32 | ```shell
33 | $ forge fmt
34 | ```
35 |
36 | ### Gas Snapshots
37 |
38 | ```shell
39 | $ forge snapshot
40 | ```
41 |
42 | ### Anvil
43 |
44 | ```shell
45 | $ anvil
46 | ```
47 |
48 | ### Deploy
49 |
50 | ```shell
51 | $ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key
52 | ```
53 |
54 | ### Cast
55 |
56 | ```shell
57 | $ cast
58 | ```
59 |
60 | ### Help
61 |
62 | ```shell
63 | $ forge --help
64 | $ anvil --help
65 | $ cast --help
66 | ```
67 |
--------------------------------------------------------------------------------
/contracts/deploy-ap-config.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -xe
4 |
5 | forge script \
6 | script/DeployAPConfig.s.sol:DeployAPConfig \
7 | --rpc-url $RPC_URL \
8 | --private-key $PRIVATE_KEY \
9 | --etherscan-api-key $ETHSCAN_API_KEY \
10 | --broadcast --verify \
11 | --slow \
12 | -vvvv
13 |
14 |
15 |
--------------------------------------------------------------------------------
/contracts/deploy-avs.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -xe
4 |
5 | forge script \
6 | script/DeployServiceManager.s.sol:DeployServiceManager \
7 | --rpc-url $RPC_URL \
8 | --private-key $PRIVATE_KEY \
9 | --etherscan-api-key $ETHSCAN_API_KEY \
10 | --broadcast --verify \
11 | --resume \
12 | --slow \
13 | -vvvv
14 |
15 |
16 |
--------------------------------------------------------------------------------
/contracts/deploy-new-avs-impl.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -xe
4 |
5 | forge script \
6 | script/DeployServiceManagerImpl.s.sol:DeployServiceManagerImpl \
7 | --rpc-url $RPC_URL \
8 | --private-key $PRIVATE_KEY \
9 | --etherscan-api-key $ETHSCAN_API_KEY \
10 | --broadcast --verify \
11 | -vvvv
12 |
--------------------------------------------------------------------------------
/contracts/deploy-pause-registry.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -xe
4 |
5 | forge script \
6 | script/DeployPauserRegistry.s.sol:DeployPauserRegistry \
7 | --rpc-url $RPC_URL \
8 | --private-key $PRIVATE_KEY \
9 | --etherscan-api-key $ETHSCAN_API_KEY \
10 | --broadcast --verify \
11 | -vvvv
12 |
13 |
14 |
--------------------------------------------------------------------------------
/contracts/deploy-task-manager-impl.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -xe
4 |
5 | forge script \
6 | script/DeployTaskManagerImpl.s.sol:DeployTaskManagerImpl \
7 | --rpc-url $RPC_URL \
8 | --private-key $PRIVATE_KEY \
9 | --etherscan-api-key $ETHSCAN_API_KEY \
10 | --broadcast --verify \
11 | -vvvv
12 |
--------------------------------------------------------------------------------
/contracts/foundry.toml:
--------------------------------------------------------------------------------
1 | [profile.default]
2 | src = "src"
3 | out = "out"
4 | libs = ["lib"]
5 | fs_permissions = [{ access = "read-write", path = "./"}]
6 |
7 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
8 | remappings = [
9 | "@eigenlayer-core/=lib/eigenlayer-middleware/lib/eigenlayer-contracts/src/",
10 | "@eigenlayer-scripts/=lib/eigenlayer-middleware/lib/eigenlayer-contracts/script/",
11 | "@eigenlayer-middleware/=lib/eigenlayer-middleware/src",
12 | "@oak-automation/=src/",
13 | "@openzeppelin/=lib/eigenlayer-middleware/lib/eigenlayer-contracts/lib/openzeppelin-contracts/",
14 | "@openzeppelin-upgrades/=lib/eigenlayer-middleware/lib/eigenlayer-contracts/lib/openzeppelin-contracts-upgradeable/",
15 | "forge-std/=lib/forge-std/src/"
16 | ]
17 |
18 | gas_reports = ["*"]
19 |
20 | # A list of ignored solc error codes
21 | # Enables or disables the optimizer
22 | optimizer = true
23 | # The number of optimizer runs
24 | optimizer_runs = 200
25 | # Whether or not to use the Yul intermediate representation compilation pipeline
26 | via_ir = false
27 | solc_version = "0.8.25"
28 |
29 | [fmt] # See https://book.getfoundry.sh/reference/config/formatter
30 | int_types = "short"
31 | line_length = 120
32 | bracket_spacing = true
33 | multiline_func_header = "all"
34 | number_underscore = "thousands"
35 | quote_style = "single"
36 | ignore = ['./script/**/*']
37 |
--------------------------------------------------------------------------------
/contracts/generate-go-bindings.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | function create_binding {
4 | contract_dir=$1
5 | contract=$2
6 | binding_dir=$3
7 | echo "generating bindings for" $contract
8 | mkdir -p $binding_dir/${contract}
9 | contract_json="$contract_dir/out/${contract}.sol/${contract}.json"
10 | solc_abi=$(cat ${contract_json} | jq -r '.abi')
11 | solc_bin=$(cat ${contract_json} | jq -r '.bytecode.object')
12 |
13 | mkdir -p data
14 | echo ${solc_abi} >data/tmp.abi
15 | echo ${solc_bin} >data/tmp.bin
16 |
17 | rm -f $binding_dir/${contract}/binding.go
18 | abigen --bin=data/tmp.bin --abi=data/tmp.abi --pkg=contract${contract} --out=$binding_dir/${contract}/binding.go
19 | rm -rf ../data/tmp.abi ../data/tmp.bin
20 | }
21 |
22 | rm -rf bindings/*
23 | forge clean
24 | forge build
25 |
26 | avs_service_contracts="AutomationTaskManager AutomationServiceManager"
27 | for contract in $avs_service_contracts; do
28 | create_binding . $contract ./bindings
29 | done
30 |
31 | # create_binding . ERC20Mock ./bindings
--------------------------------------------------------------------------------
/contracts/script/DeployAPConfig.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.12;
3 |
4 | import "forge-std/Script.sol";
5 |
6 | import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
7 | import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
8 |
9 | import {APConfig} from "../src/core/APConfig.sol";
10 |
11 | // To deploy and swap the implementation set 2 envs:
12 | // CREATE_PROXY=false
13 | // AP_PROXY_ADDRESS=0x123
14 | // PROXY_ADMIN_ADDRESS=0x456
15 | // When seeing the two env, the script will upgrade the underlying contract
16 | //
17 | // Example:
18 | // AP_PROXY_ADDRESS=0xb8abbb082ecaae8d1cd68378cf3b060f6f0e07eb \
19 | // SWAP_IMPL=true bash deploy-ap-config.sh
20 | contract DeployAPConfig is Script {
21 | function run() external {
22 | address oakAVSProxyAdmin = vm.envAddress("PROXY_ADMIN_ADDRESS");
23 | bool swapImpl = vm.envBool("SWAP_IMPL");
24 |
25 | vm.startBroadcast();
26 |
27 | string memory output = "APConfig deployment output";
28 |
29 | // 1. Deploy the implementation
30 | APConfig apConfig = new APConfig();
31 | vm.serializeAddress(output, "apConfigImpl", address(apConfig));
32 |
33 | // 2. Swap impl or deploy new proxy and bind to
34 | if (swapImpl) {
35 | ProxyAdmin oakProxyAdmin =
36 | ProxyAdmin(oakAVSProxyAdmin);
37 |
38 | // Load existing proxy and upgrade that to this new impl
39 | address apProxyAddress = vm.envAddress("AP_PROXY_ADDRESS");
40 | oakProxyAdmin.upgrade(
41 | TransparentUpgradeableProxy(payable(apProxyAddress)),
42 | address(apConfig)
43 | );
44 | } else {
45 | // Deploy the proxy contract, bind it to the impl
46 | TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
47 | address(apConfig),
48 | address(oakAVSProxyAdmin),
49 | ""
50 | );
51 | vm.serializeAddress(output, "apConfigProxy", address(proxy));
52 | }
53 |
54 | string memory registryJson = vm.serializeString(output, "object", output);
55 | vm.writeJson(registryJson, "./script/output/ap_config.json");
56 |
57 | vm.stopBroadcast();
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/contracts/script/DeployPauserRegistry.s.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.8.25;
2 |
3 | import "forge-std/Script.sol";
4 | import "@eigenlayer-core/contracts/permissions/PauserRegistry.sol";
5 |
6 | contract DeployPauserRegistry is Script {
7 | function run() external {
8 | string memory defaultRegistryPath = "./script/output/pause_registry_deploy_output.json";
9 | string memory deployedRegistryPath = vm.envOr("REGISTRY_PATH", defaultRegistryPath);
10 |
11 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
12 | address pauserAddress = vm.envAddress("PAUSER_ADDRESS");
13 |
14 | address[] memory pausers = new address[](1);
15 | pausers[0] = pauserAddress;
16 |
17 | vm.startBroadcast(deployerPrivateKey);
18 |
19 | PauserRegistry r = new PauserRegistry(pausers, pauserAddress);
20 |
21 | vm.stopBroadcast();
22 |
23 | vm.createDir("./script/output", true);
24 |
25 | string memory output = "pauser registry info deployment output";
26 | vm.serializeAddress(output, "pauserRegistryAddress", address(r));
27 |
28 | string memory registryJson = vm.serializeString(output, "object", output);
29 | vm.writeJson(registryJson, deployedRegistryPath);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/contracts/script/DeployServiceManagerImpl.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.12;
3 |
4 | import "forge-std/Script.sol";
5 | import {AutomationServiceManager} from "../src/core/AutomationServiceManager.sol";
6 | import {IStakeRegistry} from "@eigenlayer-middleware/interfaces/IStakeRegistry.sol";
7 | import {IRegistryCoordinator} from "@eigenlayer-middleware/interfaces/IRegistryCoordinator.sol";
8 | import {IAVSDirectory} from "@eigenlayer-core/contracts/interfaces/IAVSDirectory.sol";
9 |
10 | contract DeployServiceManagerImpl is Script {
11 | function run() external {
12 | address avsDirectory = vm.envAddress("AVS_DIRECTORY");
13 |
14 | // RegistryCoordinator and StakeRegistry is deployed separately
15 | address registryCoordinator = vm.envAddress("REGISTRY_COORDINATOR_ADDRESS");
16 | address stakeRegistry = vm.envAddress("STAKE_REGISTRY_ADDRESS");
17 |
18 | vm.startBroadcast();
19 |
20 | AutomationServiceManager automationServiceManagerImplementation = new AutomationServiceManager(
21 | IAVSDirectory(avsDirectory), IRegistryCoordinator(registryCoordinator), IStakeRegistry(stakeRegistry)
22 | );
23 |
24 | vm.stopBroadcast();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/contracts/script/DeployTaskManagerImpl.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.12;
3 |
4 | import "forge-std/Script.sol";
5 | import {RegistryCoordinator} from "@eigenlayer-middleware/RegistryCoordinator.sol";
6 |
7 | import {AutomationTaskManager} from "../src/core/AutomationTaskManager.sol";
8 |
9 | contract DeployTaskManagerImpl is Script {
10 | function run() external {
11 | address registryCoordinator = vm.envAddress("REGISTRY_COORDINATOR_ADDRESS");
12 |
13 | vm.startBroadcast();
14 |
15 | AutomationTaskManager automationTaskManager = new AutomationTaskManager(
16 | RegistryCoordinator(registryCoordinator), 12
17 | );
18 |
19 | vm.stopBroadcast();
20 |
21 | string memory output = "task manager info deployment output";
22 | vm.serializeAddress(output, "automationTaskManagerImpl", address(automationTaskManager));
23 |
24 | string memory registryJson = vm.serializeString(output, "object", output);
25 | vm.writeJson(registryJson, "./script/output/deploy_task_manager_impl.json");
26 |
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/contracts/script/UpgradeServiceManager.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.12;
3 |
4 | import "forge-std/Script.sol";
5 | import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
6 | import {AutomationServiceManager} from "../src/core/AutomationServiceManager.sol";
7 |
8 | contract UpgradeServiceManager is Script {
9 | function run() external {
10 | address proxyAdmin = vm.envAddress("PROXY_ADMIN_ADDRESS");
11 |
12 | address automationServiceManager = vm.envAddress("SERVICE_MANAGER_PROXY_ADDRESS");
13 |
14 | address newServiceManagerAddress = vm.envAddress("NEW_SERVICE_MANAGER_ADDRESS");
15 |
16 | ProxyAdmin oakProxyAdmin = ProxyAdmin(proxyAdmin);
17 |
18 | vm.startBroadcast();
19 |
20 | oakProxyAdmin.upgrade(
21 | TransparentUpgradeableProxy(payable(address(automationServiceManager))), address(newServiceManagerAddress)
22 | );
23 | vm.stopBroadcast();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/contracts/src/core/APConfig.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.8.12;
2 |
3 | import "../interfaces/IAPConfig.sol";
4 |
5 | contract APConfig is IAPConfig {
6 | // Mapping from operator address to alias address
7 | mapping(address => address) private operatorToAlias;
8 | mapping(address => address) private aliasToOperator;
9 |
10 | // Function to declare an alias for the operator
11 | function declareAlias(address _alias) external override {
12 | require(_alias != address(0), "Alias address cannot be the zero address");
13 | require(_alias != msg.sender, "Alias address cannot be the same with operator address");
14 |
15 | operatorToAlias[msg.sender] = _alias;
16 | aliasToOperator[_alias] = msg.sender;
17 |
18 | emit AliasDeclared(msg.sender, _alias);
19 | }
20 |
21 | // Function to undeclare an alias for the operator
22 | function undeclare() external override {
23 | require(aliasToOperator[msg.sender] != address(0), "No alias declared for this operator");
24 |
25 | delete operatorToAlias[aliasToOperator[msg.sender]];
26 | delete aliasToOperator[msg.sender];
27 |
28 | emit AliasUndeclared(msg.sender);
29 | }
30 |
31 | // Function to get the alias of an operator
32 | function getAlias(address _operator) external view override returns (address) {
33 | return operatorToAlias[_operator];
34 | }
35 |
36 | function getOperatorForAlias(address _alias) external view override returns (address) {
37 | return aliasToOperator[_alias];
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/contracts/src/core/AutomationServiceManagerStorage.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.8.12;
2 |
3 | import { EnumerableSet } from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol';
4 | import { IAutomationTaskManager } from '../interfaces/IAutomationTaskManager.sol';
5 |
6 | abstract contract AutomationServiceManagerStorage {
7 | IAutomationTaskManager public automationTaskManager;
8 |
9 | address public whitelister;
10 |
11 | EnumerableSet.AddressSet internal _operators;
12 | }
13 |
--------------------------------------------------------------------------------
/contracts/src/interfaces/IAPConfig.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.9;
3 |
4 | interface IAPConfig {
5 | // Event emitted when an alias is declared
6 | event AliasDeclared(address indexed operator, address indexed aliasAddress);
7 |
8 | // Event emitted when an alias is undeclared
9 | event AliasUndeclared(address indexed operator);
10 |
11 | // Function to declare an alias for the operator
12 | function declareAlias(address _alias) external;
13 |
14 | // Function to undeclare an alias for the operator
15 | function undeclare() external;
16 |
17 | // Function to get the alias of an operator
18 | function getAlias(address _operator) external view returns (address);
19 | function getOperatorForAlias(address _alias) external view returns (address);
20 | }
21 |
--------------------------------------------------------------------------------
/contracts/src/interfaces/IAutomationServiceManager.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity =0.8.12;
3 |
4 | import { IServiceManager } from '@eigenlayer-middleware/interfaces/IServiceManager.sol';
5 | import { BLSSignatureChecker } from '@eigenlayer-middleware/BLSSignatureChecker.sol';
6 |
7 | /**
8 | * @title Interface for the MachServiceManager contract.
9 | * @author Altlayer, Inc.
10 | */
11 | interface IAutomationServiceManager is IServiceManager {
12 | /**
13 | * @notice Emitted when an operator is added to the MachServiceManagerAVS.
14 | * @param operator The address of the operator
15 | */
16 | event OperatorAdded(address indexed operator);
17 |
18 | /**
19 | * @notice Emitted when an operator is removed from the MachServiceManagerAVS.
20 | * @param operator The address of the operator
21 | */
22 | event OperatorRemoved(address indexed operator);
23 |
24 | event TaskManagerUpdate(
25 | address newTaskManager,
26 | address previousTaskManager
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/contracts/src/interfaces/IAutomationTaskManager.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.9;
3 |
4 | import '@eigenlayer-middleware/libraries/BN254.sol';
5 |
6 | interface IAutomationTaskManager {
7 | // EVENTS
8 | event NewTaskCreated(uint32 indexed taskIndex, Task task);
9 |
10 | event TaskResponded(TaskResponse taskResponse, TaskResponseMetadata taskResponseMetadata);
11 |
12 | event TaskCompleted(uint32 indexed taskIndex);
13 |
14 | event TaskChallengedSuccessfully(uint32 indexed taskIndex, address indexed challenger);
15 |
16 | event TaskChallengedUnsuccessfully(uint32 indexed taskIndex, address indexed challenger);
17 |
18 | // STRUCTS
19 | struct Task {
20 | uint numberToBeSquared;
21 | uint32 taskCreatedBlock;
22 | // task submitter decides on the criteria for a task to be completed
23 | // note that this does not mean the task was "correctly" answered (i.e. the number was squared correctly)
24 | // this is for the challenge logic to verify
25 | // task is completed (and contract will accept its TaskResponse) when each quorumNumbers specified here
26 | // are signed by at least quorumThresholdPercentage of the operators
27 | // note that we set the quorumThresholdPercentage to be the same for all quorumNumbers, but this could be changed
28 | bytes quorumNumbers;
29 | uint32 quorumThresholdPercentage;
30 | }
31 |
32 | // Task response is hashed and signed by operators.
33 | // these signatures are aggregated and sent to the contract as response.
34 | struct TaskResponse {
35 | // Can be obtained by the operator from the event NewTaskCreated.
36 | uint32 referenceTaskIndex;
37 | // This is just the response that the operator has to compute by itself.
38 | uint numberSquared;
39 | }
40 |
41 | // Extra information related to taskResponse, which is filled inside the contract.
42 | // It thus cannot be signed by operators, so we keep it in a separate struct than TaskResponse
43 | // This metadata is needed by the challenger, so we emit it in the TaskResponded event
44 | struct TaskResponseMetadata {
45 | uint32 taskResponsedBlock;
46 | bytes32 hashOfNonSigners;
47 | }
48 |
49 | // FUNCTIONS
50 | // NOTE: this function creates new task.
51 | function createNewTask(
52 | uint numberToBeSquared,
53 | uint32 quorumThresholdPercentage,
54 | bytes calldata quorumNumbers
55 | )
56 | external;
57 |
58 | /// @notice Returns the current 'taskNumber' for the middleware
59 | function taskNumber() external view returns (uint32);
60 |
61 | // // NOTE: this function raises challenge to existing tasks.
62 | function raiseAndResolveChallenge(
63 | Task calldata task,
64 | TaskResponse calldata taskResponse,
65 | TaskResponseMetadata calldata taskResponseMetadata,
66 | BN254.G1Point[] memory pubkeysOfNonSigningOperators
67 | )
68 | external;
69 |
70 | /// @notice Returns the TASK_RESPONSE_WINDOW_BLOCK
71 | function getTaskResponseWindowBlock() external view returns (uint32);
72 | }
73 |
--------------------------------------------------------------------------------
/contracts/upgrade-avs-impl.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -xe
4 |
5 | forge script \
6 | script/UpgradeServiceManager.s.sol:UpgradeServiceManager \
7 | --rpc-url $RPC_URL \
8 | --private-key $PRIVATE_KEY \
9 | --etherscan-api-key $ETHSCAN_API_KEY \
10 | --broadcast --verify \
11 | -vvvv
12 |
--------------------------------------------------------------------------------
/core/apqueue/cleanup.go:
--------------------------------------------------------------------------------
1 | package apqueue
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | // CleanupStats holds statistics about the cleanup operation
8 | type CleanupStats struct {
9 | TotalJobs int
10 | OrphanedJobs int
11 | RemovedJobs int
12 | FailedCleanup int
13 | Duration time.Duration
14 | }
15 |
16 | // CleanupOrphanedJobs removes jobs for tasks that no longer exist
17 | func (q *Queue) CleanupOrphanedJobs() (*CleanupStats, error) {
18 | startTime := time.Now()
19 | stats := &CleanupStats{}
20 |
21 | q.logger.Info("starting orphaned jobs cleanup")
22 |
23 | // Get all jobs from different queues
24 | for _, status := range []jobStatus{jobPending, jobInProgress, jobFailed} {
25 | prefix := q.getQueueKeyPrefix(status)
26 |
27 | // Get all jobs with this status
28 | kvs, err := q.db.GetByPrefix(prefix)
29 | if err != nil {
30 | q.logger.Error("failed to get jobs for cleanup", "status", status.HumanReadable(), "error", err)
31 | stats.FailedCleanup++
32 | continue
33 | }
34 |
35 | for _, kv := range kvs {
36 | stats.TotalJobs++
37 |
38 | // Decode the job to get task ID
39 | job, err := decodeJob(kv.Value)
40 | if err != nil {
41 | q.logger.Error("failed to decode job during cleanup", "key", string(kv.Key), "error", err)
42 | stats.FailedCleanup++
43 | continue
44 | }
45 |
46 | // Check if task still exists
47 | taskKey := []byte("t:a:" + job.Name) // Check active tasks
48 | _, err = q.db.GetKey(taskKey)
49 | if err != nil {
50 | // Task doesn't exist - this is an orphaned job
51 | stats.OrphanedJobs++
52 |
53 | q.dbLock.Lock()
54 | if delErr := q.db.Delete(kv.Key); delErr != nil {
55 | q.logger.Error("failed to remove orphaned job", "job_id", job.ID, "task_id", job.Name, "error", delErr)
56 | stats.FailedCleanup++
57 | } else {
58 | stats.RemovedJobs++
59 | q.logger.Debug("removed orphaned job", "job_id", job.ID, "task_id", job.Name, "status", status.HumanReadable())
60 | }
61 | q.dbLock.Unlock()
62 | }
63 | }
64 | }
65 |
66 | stats.Duration = time.Since(startTime)
67 |
68 | q.logger.Info("orphaned jobs cleanup completed",
69 | "total_jobs", stats.TotalJobs,
70 | "orphaned_jobs", stats.OrphanedJobs,
71 | "removed_jobs", stats.RemovedJobs,
72 | "failed_cleanup", stats.FailedCleanup,
73 | "duration_ms", stats.Duration.Milliseconds())
74 |
75 | return stats, nil
76 | }
77 |
78 | // SchedulePeriodicCleanup runs cleanup every interval
79 | func (q *Queue) SchedulePeriodicCleanup(interval time.Duration) {
80 | ticker := time.NewTicker(interval)
81 | go func() {
82 | for {
83 | select {
84 | case <-ticker.C:
85 | if stats, err := q.CleanupOrphanedJobs(); err != nil {
86 | q.logger.Error("periodic cleanup failed", "error", err)
87 | } else if stats.RemovedJobs > 0 {
88 | q.logger.Info("periodic cleanup removed orphaned jobs",
89 | "removed_jobs", stats.RemovedJobs)
90 | }
91 | case <-q.closeCh:
92 | ticker.Stop()
93 | return
94 | }
95 | }
96 | }()
97 | }
98 |
--------------------------------------------------------------------------------
/core/apqueue/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | AP Queue is a distributed queue with a combination of off-chain and on-chain storage to enqueue and execute on-chain transaction
3 | */
4 | package apqueue
5 |
--------------------------------------------------------------------------------
/core/apqueue/job.go:
--------------------------------------------------------------------------------
1 | package apqueue
2 |
3 | import (
4 | "encoding/json"
5 | )
6 |
7 | // jobStatus Enum Type
8 | type jobStatus string
9 |
10 | const (
11 | // Use a single char to save space
12 | // jobPending : waiting to be processed
13 | jobPending jobStatus = "q"
14 | // jobInProgress : processing in progress
15 | jobInProgress = "p"
16 | // jobComplete : processing complete
17 | jobComplete = "c"
18 | // jobFailed : processing errored out
19 | jobFailed = "f"
20 | )
21 |
22 | // String returns a human-readable status name for logging
23 | func (s jobStatus) String() string {
24 | switch s {
25 | case jobPending:
26 | return "pending"
27 | case jobInProgress:
28 | return "in_progress"
29 | case jobComplete:
30 | return "complete"
31 | case jobFailed:
32 | return "failed"
33 | default:
34 | return string(s) // fallback to raw value
35 | }
36 | }
37 |
38 | // HumanReadable returns a descriptive status name for logging
39 | func (s jobStatus) HumanReadable() string {
40 | switch s {
41 | case jobPending:
42 | return "⏳ pending"
43 | case jobInProgress:
44 | return "🔄 in_progress"
45 | case jobComplete:
46 | return "✅ complete"
47 | case jobFailed:
48 | return "❌ failed"
49 | default:
50 | return string(s) // fallback to raw value
51 | }
52 | }
53 |
54 | type Job struct {
55 | Type string `json:"t"`
56 | Name string `json:"n"`
57 | Data []byte `json:"d"`
58 |
59 | EnqueueAt uint64 `json:"q,omitempty"`
60 | Attempt uint64 `json:"a,omitempty"`
61 |
62 | // id of the job in the queue system
63 | // This ID is generate by this package in a sequence. The ID is guarantee to
64 | // be unique and can be used as part of the signature as nonce
65 | ID uint64 `json:"id"`
66 | }
67 |
68 | func encodeJob(j *Job) ([]byte, error) {
69 | return json.Marshal(j)
70 | }
71 |
72 | func decodeJob(b []byte) (*Job, error) {
73 | j := Job{}
74 | err := json.Unmarshal(b, &j)
75 | return &j, err
76 | }
77 |
--------------------------------------------------------------------------------
/core/apqueue/worker.go:
--------------------------------------------------------------------------------
1 | package apqueue
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/AvaProtocol/EigenLayer-AVS/storage"
8 |
9 | sdklogging "github.com/Layr-Labs/eigensdk-go/logging"
10 | )
11 |
12 | type JobProcessor interface {
13 | Perform(j *Job) error
14 | }
15 |
16 | type Worker struct {
17 | q *Queue
18 | db storage.Storage
19 |
20 | processorRegistry map[string]JobProcessor
21 | logger sdklogging.Logger
22 | }
23 |
24 | func (w *Worker) RegisterProcessor(jobType string, processor JobProcessor) error {
25 | w.processorRegistry[jobType] = processor
26 |
27 | return nil
28 | }
29 |
30 | // A worker monitors queue, and use a processor to perform job
31 | func NewWorker(q *Queue, db storage.Storage) *Worker {
32 | w := &Worker{
33 | q: q,
34 | db: db,
35 | logger: q.logger,
36 |
37 | processorRegistry: make(map[string]JobProcessor),
38 | }
39 |
40 | return w
41 | }
42 |
43 | // isOrphanedJob checks if the error indicates a deleted/missing task
44 | func (w *Worker) isOrphanedJob(err error) bool {
45 | errorMsg := err.Error()
46 | return strings.Contains(errorMsg, "task not found in storage") ||
47 | strings.Contains(errorMsg, "task may have been deleted") ||
48 | strings.Contains(errorMsg, "storage key is incorrect")
49 | }
50 |
51 | // wake up and pop first item in the queue to process
52 | func (w *Worker) ProcessSignal(jid uint64) {
53 | job, err := w.q.Dequeue()
54 | if err != nil {
55 | w.logger.Error("failed to dequeue", "error", err)
56 | return
57 | }
58 |
59 | // Check if job is nil (queue is empty)
60 | if job == nil {
61 | w.logger.Debug("no jobs available in queue", "job_id", jid)
62 | return
63 | }
64 |
65 | // Single consolidated log for job processing start
66 | w.logger.Info("processing job", "job_id", jid, "task_id", job.Name, "job_type", job.Type)
67 |
68 | processor, ok := w.processorRegistry[job.Type]
69 | if ok {
70 | err = processor.Perform(job)
71 | } else {
72 | w.logger.Warn("unsupported job type", "job_id", jid, "task_id", job.Name, "job_type", job.Type)
73 | err = fmt.Errorf("unsupported job type: %s", job.Type)
74 | }
75 |
76 | if err == nil {
77 | if markErr := w.q.markJobDone(job, jobComplete); markErr != nil {
78 | w.logger.Error("failed to mark job as complete", "error", markErr, "job_id", jid)
79 | } else {
80 | w.logger.Info("job completed successfully", "job_id", jid, "task_id", job.Name)
81 | }
82 | } else {
83 | // Check if this is an orphaned job (task deleted)
84 | isOrphanedJob := w.isOrphanedJob(err)
85 | if isOrphanedJob {
86 | // Mark as permanently failed - don't retry orphaned jobs
87 | if markErr := w.q.markJobDone(job, jobFailed); markErr != nil {
88 | w.logger.Error("failed to mark orphaned job as failed", "error", markErr, "job_id", jid)
89 | }
90 | // Use Info level instead of Warn to avoid stack traces
91 | w.logger.Info("orphaned job removed (task deleted)",
92 | "job_id", jid,
93 | "task_id", job.Name,
94 | "reason", "task_not_found")
95 | } else {
96 | // Regular failure - could be retried
97 | if markErr := w.q.markJobDone(job, jobFailed); markErr != nil {
98 | w.logger.Error("failed to mark job as failed", "error", markErr, "job_id", jid)
99 | }
100 | w.logger.Error("job processing failed",
101 | "job_id", jid,
102 | "task_id", job.Name,
103 | "job_type", job.Type,
104 | "error", err.Error())
105 | }
106 | }
107 | }
108 |
109 | func (w *Worker) loop() {
110 | for {
111 | select {
112 | case jid := <-w.q.eventCh:
113 | w.ProcessSignal(jid)
114 | case <-w.q.closeCh: // loop was stopped
115 | return
116 | }
117 | }
118 | }
119 |
120 | func (w *Worker) MustStart() {
121 | go func() {
122 | w.loop()
123 | }()
124 | }
125 |
--------------------------------------------------------------------------------
/core/auth/errors.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | const (
4 | AuthenticationError = "User authentication error"
5 | InvalidSignatureFormat = "Unable to decode client's signature. Please check the message's format."
6 | InvalidAuthenticationKey = "User Auth key is invalid"
7 | InvalidAPIKey = "API key is invalid"
8 | MalformedExpirationTime = "Malformed Expired Time"
9 | )
10 |
--------------------------------------------------------------------------------
/core/auth/operator.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "context"
5 | "crypto/ecdsa"
6 | "fmt"
7 | "time"
8 |
9 | "github.com/AvaProtocol/EigenLayer-AVS/core/chainio/signer"
10 | "github.com/ethereum/go-ethereum/common"
11 | )
12 |
13 | type ClientAuth struct {
14 | EcdsaPrivateKey *ecdsa.PrivateKey
15 | SignerAddr common.Address
16 | }
17 |
18 | // Return value is mapped to request headers.
19 | func (a ClientAuth) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
20 | epoch := time.Now().Unix()
21 | token, err := signer.SignMessageAsHex(
22 | a.EcdsaPrivateKey,
23 | GetOperatorSigninMessage(a.SignerAddr.String(), epoch),
24 | )
25 |
26 | if err != nil {
27 | panic(err)
28 | }
29 |
30 | return map[string]string{
31 | "authorization": fmt.Sprintf("Bearer %d.%s", epoch, token),
32 | }, nil
33 | }
34 |
35 | func (ClientAuth) RequireTransportSecurity() bool {
36 | // TODO: Toggle true on prod
37 | return false
38 | }
39 |
--------------------------------------------------------------------------------
/core/auth/protocol.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/golang-jwt/jwt/v5"
7 | )
8 |
9 | const (
10 | Issuer = "AvaProtocol"
11 | JwtAlg = "HS256"
12 |
13 | AdminRole = ApiRole("admin")
14 | ReadonlyRole = ApiRole("readonly")
15 | )
16 |
17 | var (
18 | ErrorUnAuthorized = fmt.Errorf("Unauthorized error")
19 |
20 | ErrorInvalidToken = fmt.Errorf("Invalid Bearer Token")
21 |
22 | ErrorMalformedAuthHeader = fmt.Errorf("Malform auth header")
23 | ErrorExpiredSignature = fmt.Errorf("Signature is expired")
24 | )
25 |
26 | type ApiRole string
27 | type APIClaim struct {
28 | *jwt.RegisteredClaims
29 | Roles []ApiRole `json:"roles"`
30 | }
31 |
32 | func GetOperatorSigninMessage(operatorAddr string, epoch int64) []byte {
33 | return []byte(fmt.Sprintf("Operator:%sEpoch:%d", operatorAddr, epoch))
34 | }
35 |
--------------------------------------------------------------------------------
/core/auth/server.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | "time"
8 |
9 | "github.com/AvaProtocol/EigenLayer-AVS/core/chainio/signer"
10 | )
11 |
12 | // VerifyOperator checks and confirm that the auth header is indeed signed by
13 | // the operatorAddr
14 | func VerifyOperator(authHeader string, operatorAddr string) (bool, error) {
15 | bearerToken := strings.SplitN(authHeader, " ", 2)
16 | if len(bearerToken) < 2 || bearerToken[0] != "Bearer" {
17 | return false, ErrorInvalidToken
18 | }
19 |
20 | tokens := strings.SplitN(bearerToken[1], ".", 2)
21 | if len(tokens) < 2 {
22 | return false, ErrorMalformedAuthHeader
23 | }
24 | epoch, _ := strconv.Atoi(tokens[0])
25 | if time.Now().Add(-10*time.Second).Unix() > int64(epoch) {
26 | return false, ErrorExpiredSignature
27 | }
28 |
29 | result, err := signer.Verify(GetOperatorSigninMessage(operatorAddr, int64(epoch)), tokens[1], operatorAddr)
30 | if err == nil {
31 | return result, nil
32 | }
33 |
34 | return result, fmt.Errorf("unauthorized error: %w", err)
35 | }
36 |
--------------------------------------------------------------------------------
/core/auth/telegram.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | // Handle auththentication process between Telegram Bot and the GRPC for task
4 | // management
5 | type TelegramAuth struct {
6 | }
7 |
--------------------------------------------------------------------------------
/core/auth/user.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/ethereum/go-ethereum/common"
7 | "github.com/golang-jwt/jwt/v5"
8 | )
9 |
10 | // GetUserFromKeyOrSignature attempts to verify that the payload is a valid JWT
11 | // token for a particular EOA, or the payload is the right signature of an EOA
12 | func GetUserFromKeyOrSignature(payload string) *common.Address {
13 | return nil
14 | }
15 |
16 | // VerifyJwtKeyForUser checks that the JWT key is either for this user wallet,
17 | // or the JWT key for an API key that can manage the wallet
18 | func VerifyJwtKeyForUser(secret []byte, key string, userWallet common.Address) (bool, error) {
19 | // Parse takes the token string and a function for looking up the key. The
20 | // latter is especially
21 | // useful if you use multiple keys for your application. The standard is to use
22 | // 'kid' in the
23 | // head of the token to identify which key to use, but the parsed token (head
24 | // and claims) is provided
25 | // to the callback, providing flexibility.
26 | token, err := jwt.Parse(key, func(token *jwt.Token) (interface{}, error) {
27 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
28 | return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
29 | }
30 |
31 | if token.Header["alg"] != JwtAlg {
32 | return nil, fmt.Errorf("invalid signing algorithm")
33 | }
34 |
35 | return secret, nil
36 | })
37 |
38 | if err != nil {
39 | return false, err
40 | }
41 |
42 | if token.Header["alg"] != JwtAlg {
43 | return false, fmt.Errorf("invalid signing algorithm")
44 | }
45 |
46 | claims, ok := token.Claims.(jwt.MapClaims)
47 | if ok {
48 | subVal, ok := claims["sub"]
49 | if !ok {
50 | return false, fmt.Errorf("Missing subject claim")
51 | }
52 |
53 | sub, ok := subVal.(string)
54 | if !ok {
55 | return false, fmt.Errorf("Subject is not a string")
56 | }
57 |
58 | if sub == "" {
59 | return false, fmt.Errorf("Missing subject")
60 | }
61 |
62 | if sub == "apikey" {
63 | roles := []ApiRole{}
64 | rolesVal, ok := claims["roles"]
65 | if !ok {
66 | return false, fmt.Errorf("Missing roles claim")
67 | }
68 |
69 | rolesArray, ok := rolesVal.([]any)
70 | if !ok {
71 | return false, fmt.Errorf("Roles is not an array")
72 | }
73 |
74 | for _, v := range rolesArray {
75 | roleStr, ok := v.(string)
76 | if !ok {
77 | continue // Skip non-string roles
78 | }
79 | roles = append(roles, ApiRole(roleStr))
80 | }
81 | if claims["roles"] == nil || !contains(roles, "admin") {
82 | return false, fmt.Errorf("Invalid API Key")
83 | }
84 |
85 | return true, nil
86 | }
87 |
88 | claimAddress := common.HexToAddress(sub)
89 | if claimAddress != userWallet {
90 | return false, fmt.Errorf("Invalid Subject")
91 | }
92 | }
93 |
94 | return false, fmt.Errorf("Malform JWT Key Claim")
95 | }
96 |
97 | // contains checks if a slice contains a specific value (replacement for slices.Contains)
98 | func contains(slice []ApiRole, val ApiRole) bool {
99 | for _, item := range slice {
100 | if item == val {
101 | return true
102 | }
103 | }
104 | return false
105 | }
106 |
107 | // contains is already defined in protocol.go
108 |
--------------------------------------------------------------------------------
/core/backup/backup.go:
--------------------------------------------------------------------------------
1 | package backup
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | "time"
9 |
10 | "github.com/AvaProtocol/EigenLayer-AVS/storage"
11 | "github.com/Layr-Labs/eigensdk-go/logging"
12 | )
13 |
14 | type Service struct {
15 | logger logging.Logger
16 | db storage.Storage
17 | backupDir string
18 | backupEnabled bool
19 | interval time.Duration
20 | stop chan struct{}
21 | }
22 |
23 | func NewService(logger logging.Logger, db storage.Storage, backupDir string) *Service {
24 | return &Service{
25 | logger: logger,
26 | db: db,
27 | backupDir: backupDir,
28 | backupEnabled: false,
29 | stop: make(chan struct{}),
30 | }
31 | }
32 |
33 | func (s *Service) StartPeriodicBackup(interval time.Duration) error {
34 | if s.backupEnabled {
35 | return fmt.Errorf("backup service already running")
36 | }
37 |
38 | if err := os.MkdirAll(s.backupDir, 0755); err != nil {
39 | return fmt.Errorf("failed to create backup directory: %v", err)
40 | }
41 |
42 | s.interval = interval
43 | s.backupEnabled = true
44 |
45 | go s.backupLoop()
46 |
47 | s.logger.Infof("Started periodic backup every %v to %s", interval, s.backupDir)
48 | return nil
49 | }
50 |
51 | func (s *Service) StopPeriodicBackup() {
52 | if !s.backupEnabled {
53 | return
54 | }
55 |
56 | s.backupEnabled = false
57 | close(s.stop)
58 | s.logger.Infof("Stopped periodic backup")
59 | }
60 |
61 | func (s *Service) backupLoop() {
62 | ticker := time.NewTicker(s.interval)
63 | defer ticker.Stop()
64 |
65 | for {
66 | select {
67 | case <-ticker.C:
68 | if backupFile, err := s.PerformBackup(); err != nil {
69 | s.logger.Errorf("Periodic backup failed: %v", err)
70 | } else {
71 | s.logger.Infof("Periodic backup completed successfully to %s", backupFile)
72 | }
73 | case <-s.stop:
74 | return
75 | }
76 | }
77 | }
78 |
79 | func (s *Service) PerformBackup() (string, error) {
80 | timestamp := time.Now().Format("06-01-02-15-04")
81 | backupPath := filepath.Join(s.backupDir, timestamp)
82 |
83 | if err := os.MkdirAll(backupPath, 0755); err != nil {
84 | return "", fmt.Errorf("failed to create backup timestamp directory: %v", err)
85 | }
86 |
87 | backupFile := filepath.Join(backupPath, "full-backup.db")
88 | f, err := os.Create(backupFile)
89 | if err != nil {
90 | return "", fmt.Errorf("failed to create backup file: %v", err)
91 | }
92 | defer f.Close()
93 |
94 | s.logger.Infof("Running backup to %s", backupFile)
95 | since := uint64(0) // Full backup
96 | _, err = s.db.Backup(context.Background(), f, since)
97 | if err != nil {
98 | return "", fmt.Errorf("backup operation failed: %v", err)
99 | }
100 |
101 | s.logger.Infof("Backup completed successfully to %s", backupFile)
102 | return backupFile, nil
103 | }
104 |
--------------------------------------------------------------------------------
/core/backup/backup_test.go:
--------------------------------------------------------------------------------
1 | package backup
2 |
3 | import (
4 | "os"
5 | "testing"
6 | "time"
7 |
8 | "github.com/AvaProtocol/EigenLayer-AVS/core/testutil"
9 | )
10 |
11 | func TestBackup(t *testing.T) {
12 | // Test cases for backup service
13 | t.Run("StartPeriodicBackup", func(t *testing.T) {
14 | // Setup
15 | logger := testutil.GetLogger()
16 | db := testutil.TestMustDB()
17 | tempDir := t.TempDir()
18 |
19 | service := NewService(logger, db, tempDir)
20 |
21 | // Test starting backup service
22 | err := service.StartPeriodicBackup(1 * time.Hour)
23 | if err != nil {
24 | t.Fatalf("Failed to start periodic backup: %v", err)
25 | }
26 |
27 | if !service.backupEnabled {
28 | t.Error("Backup service should be enabled after starting")
29 | }
30 |
31 | // Test starting again should fail
32 | err = service.StartPeriodicBackup(1 * time.Hour)
33 | if err == nil {
34 | t.Error("Starting backup service twice should return an error")
35 | }
36 |
37 | // Cleanup
38 | service.StopPeriodicBackup()
39 | })
40 |
41 | t.Run("StopPeriodicBackup", func(t *testing.T) {
42 | // Setup
43 | logger := testutil.GetLogger()
44 | db := testutil.TestMustDB()
45 | tempDir := t.TempDir()
46 |
47 | service := NewService(logger, db, tempDir)
48 |
49 | // Start and then stop
50 | _ = service.StartPeriodicBackup(1 * time.Hour)
51 | service.StopPeriodicBackup()
52 |
53 | if service.backupEnabled {
54 | t.Error("Backup service should be disabled after stopping")
55 | }
56 |
57 | // Test stopping when not running (should be a no-op)
58 | service.StopPeriodicBackup()
59 | })
60 |
61 | t.Run("PerformBackup", func(t *testing.T) {
62 | // Setup
63 | logger := testutil.GetLogger()
64 | db := testutil.TestMustDB()
65 | tempDir := t.TempDir()
66 |
67 | service := NewService(logger, db, tempDir)
68 |
69 | // Test performing a backup
70 | backupFile, err := service.PerformBackup()
71 | if err != nil {
72 | t.Fatalf("Failed to perform backup: %v", err)
73 | }
74 |
75 | // Verify backup file exists
76 | if _, err := os.Stat(backupFile); os.IsNotExist(err) {
77 | t.Errorf("Backup file %s does not exist", backupFile)
78 | }
79 | })
80 | }
81 |
82 | // Mock implementations for testing
83 |
--------------------------------------------------------------------------------
/core/chainio/aa/Makefile:
--------------------------------------------------------------------------------
1 | genabi:
2 | abigen --abi=../abis/entrypoint.abi --pkg=aa --type=EntryPoint --out=entrypoint.go
3 | abigen --abi=../abis/factory.abi --pkg=aa --type=SimpleFactory --out=simplefactory.go
4 | abigen --abi=../abis/account.abi --pkg=simpleaccount --type=SimpleAccount --out=simpleaccount/simpleaccount.go
5 | abigen --abi=../abis/paymaster.abi --pkg=paymaster --type=PayMaster --out=paymaster/paymaster.go
6 |
--------------------------------------------------------------------------------
/core/chainio/aa/aa.go:
--------------------------------------------------------------------------------
1 | package aa
2 |
3 | import (
4 | "fmt"
5 | "math/big"
6 | "strings"
7 |
8 | "github.com/AvaProtocol/EigenLayer-AVS/core/chainio/aa/simpleaccount"
9 | "github.com/ethereum/go-ethereum/accounts/abi"
10 | "github.com/ethereum/go-ethereum/common"
11 | "github.com/ethereum/go-ethereum/common/hexutil"
12 | "github.com/ethereum/go-ethereum/ethclient"
13 | // "github.com/ethereum/go-ethereum/accounts/abi/bind"
14 | )
15 |
16 | var (
17 | factoryABI abi.ABI
18 | defaultSalt = big.NewInt(0)
19 |
20 | simpleAccountABI *abi.ABI
21 | EntrypointAddress = common.HexToAddress("0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789")
22 | factoryAddress common.Address
23 | )
24 |
25 | func SetFactoryAddress(address common.Address) {
26 | factoryAddress = address
27 | }
28 |
29 | func SetEntrypointAddress(address common.Address) {
30 | EntrypointAddress = address
31 | }
32 |
33 | func buildFactoryABI() {
34 | var err error
35 | factoryABI, err = abi.JSON(strings.NewReader(SimpleFactoryMetaData.ABI))
36 | if err != nil {
37 | panic(fmt.Errorf("Invalid factory ABI: %w", err))
38 | }
39 | }
40 |
41 | // Get InitCode returns initcode for a given address with a given salt
42 | func GetInitCode(ownerAddress string, salt *big.Int) (string, error) {
43 | var err error
44 |
45 | buildFactoryABI()
46 |
47 | var data []byte
48 | data = append(data, factoryAddress.Bytes()...)
49 |
50 | calldata, err := factoryABI.Pack("createAccount", common.HexToAddress(ownerAddress), salt)
51 |
52 | if err != nil {
53 | return "", err
54 | }
55 |
56 | data = append(data, calldata...)
57 |
58 | return hexutil.Encode(data), nil
59 | //return common.Bytes2Hex(data), nil
60 | }
61 |
62 | func GetSenderAddress(conn *ethclient.Client, ownerAddress common.Address, salt *big.Int) (*common.Address, error) {
63 | simpleFactory, err := NewSimpleFactory(factoryAddress, conn)
64 | if err != nil {
65 | return nil, err
66 | }
67 |
68 | sender, err := simpleFactory.GetAddress(nil, ownerAddress, salt)
69 | return &sender, err
70 | }
71 |
72 | // Compute smart wallet address for a particular factory
73 | func GetSenderAddressForFactory(conn *ethclient.Client, ownerAddress common.Address, customFactoryAddress common.Address, salt *big.Int) (*common.Address, error) {
74 | simpleFactory, err := NewSimpleFactory(customFactoryAddress, conn)
75 | if err != nil {
76 | return nil, err
77 | }
78 |
79 | sender, err := simpleFactory.GetAddress(nil, ownerAddress, salt)
80 | return &sender, err
81 | }
82 |
83 | func GetNonce(conn *ethclient.Client, ownerAddress common.Address, salt *big.Int) (*big.Int, error) {
84 | if salt == nil {
85 | salt = defaultSalt
86 | }
87 |
88 | entrypoint, err := NewEntryPoint(EntrypointAddress, conn)
89 | if err != nil {
90 | return nil, err
91 | }
92 |
93 | return entrypoint.GetNonce(nil, ownerAddress, salt)
94 | }
95 |
96 | func MustNonce(conn *ethclient.Client, ownerAddress common.Address, salt *big.Int) *big.Int {
97 | nonce, e := GetNonce(conn, ownerAddress, salt)
98 | if e != nil {
99 | panic(e)
100 | }
101 |
102 | return nonce
103 | }
104 |
105 | // Generate calldata for UserOps
106 | func PackExecute(targetAddress common.Address, ethValue *big.Int, calldata []byte) ([]byte, error) {
107 | var err error
108 | if simpleAccountABI == nil {
109 | simpleAccountABI, err = simpleaccount.SimpleAccountMetaData.GetAbi()
110 | if err != nil {
111 | return nil, err
112 | }
113 | }
114 |
115 | return simpleAccountABI.Pack("execute", targetAddress, ethValue, calldata)
116 | }
117 |
--------------------------------------------------------------------------------
/core/chainio/abis/apconfig.abi:
--------------------------------------------------------------------------------
1 | [{"type":"function","name":"declareAlias","inputs":[{"name":"_alias","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getAlias","inputs":[{"name":"_operator","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getOperatorForAlias","inputs":[{"name":"_alias","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"undeclare","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"AliasDeclared","inputs":[{"name":"operator","type":"address","indexed":true,"internalType":"address"},{"name":"aliasAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"AliasUndeclared","inputs":[{"name":"operator","type":"address","indexed":true,"internalType":"address"}],"anonymous":false}]
2 |
--------------------------------------------------------------------------------
/core/chainio/abis/factory.abi:
--------------------------------------------------------------------------------
1 | [{"inputs":[{"internalType":"contract IEntryPoint","name":"_entryPoint","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"accountImplementation","outputs":[{"internalType":"contract SimpleAccount","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"salt","type":"uint256"}],"name":"createAccount","outputs":[{"internalType":"contract SimpleAccount","name":"ret","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"salt","type":"uint256"}],"name":"getAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]
2 |
--------------------------------------------------------------------------------
/core/chainio/apconfig/Makefile:
--------------------------------------------------------------------------------
1 | genabi:
2 | abigen --abi=../abis/apconfig.abi --pkg=apconfig --type=APConfig --out=binding.go
3 |
--------------------------------------------------------------------------------
/core/chainio/apconfig/apconfig.go:
--------------------------------------------------------------------------------
1 | package apconfig
2 |
3 | import (
4 | "github.com/ethereum/go-ethereum/common"
5 | "github.com/ethereum/go-ethereum/ethclient"
6 | )
7 |
8 | func GetContract(ethRpcURL string, address common.Address) (*APConfig, error) {
9 | ethRpcClient, err := ethclient.Dial(ethRpcURL)
10 | if err != nil {
11 | return nil, err
12 | }
13 |
14 | return NewAPConfig(address, ethRpcClient)
15 | }
16 |
--------------------------------------------------------------------------------
/core/chainio/avs_reader.go:
--------------------------------------------------------------------------------
1 | package chainio
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/ethereum/go-ethereum/accounts/abi/bind"
7 | gethcommon "github.com/ethereum/go-ethereum/common"
8 |
9 | sdkavsregistry "github.com/Layr-Labs/eigensdk-go/chainio/clients/avsregistry"
10 | "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth"
11 | logging "github.com/Layr-Labs/eigensdk-go/logging"
12 |
13 | cstaskmanager "github.com/AvaProtocol/EigenLayer-AVS/contracts/bindings/AutomationTaskManager"
14 | "github.com/AvaProtocol/EigenLayer-AVS/core/config"
15 | )
16 |
17 | type AvsReader struct {
18 | *sdkavsregistry.ChainReader
19 | AvsServiceBindings *AvsManagersBindings
20 | logger logging.Logger
21 | }
22 |
23 | func BuildAvsReaderFromConfig(c *config.Config) (*AvsReader, error) {
24 | return BuildAvsReader(c.AutomationRegistryCoordinatorAddr, c.OperatorStateRetrieverAddr, c.EthHttpClient, c.Logger)
25 | }
26 | func BuildAvsReader(registryCoordinatorAddr, operatorStateRetrieverAddr gethcommon.Address, ethHttpClient eth.HttpBackend, logger logging.Logger) (*AvsReader, error) {
27 | avsManagersBindings, err := NewAvsManagersBindings(registryCoordinatorAddr, operatorStateRetrieverAddr, ethHttpClient, logger)
28 | if err != nil {
29 | return nil, err
30 | }
31 | avsRegistryReader, err := sdkavsregistry.BuildAvsRegistryChainReader(registryCoordinatorAddr, operatorStateRetrieverAddr, ethHttpClient, logger)
32 | if err != nil {
33 | return nil, err
34 | }
35 | return NewAvsReader(avsRegistryReader, avsManagersBindings, logger)
36 | }
37 | func NewAvsReader(avsRegistryReader *sdkavsregistry.ChainReader, avsServiceBindings *AvsManagersBindings, logger logging.Logger) (*AvsReader, error) {
38 | return &AvsReader{
39 | ChainReader: avsRegistryReader,
40 | AvsServiceBindings: avsServiceBindings,
41 | logger: logger,
42 | }, nil
43 | }
44 |
45 | func (r *AvsReader) CheckSignatures(
46 | ctx context.Context, msgHash [32]byte, quorumNumbers []byte, referenceBlockNumber uint32, nonSignerStakesAndSignature cstaskmanager.IBLSSignatureCheckerNonSignerStakesAndSignature,
47 | ) (cstaskmanager.IBLSSignatureCheckerQuorumStakeTotals, error) {
48 | stakeTotalsPerQuorum, _, err := r.AvsServiceBindings.TaskManager.CheckSignatures(
49 | &bind.CallOpts{}, msgHash, quorumNumbers, referenceBlockNumber, nonSignerStakesAndSignature,
50 | )
51 | if err != nil {
52 | return cstaskmanager.IBLSSignatureCheckerQuorumStakeTotals{}, err
53 | }
54 | return stakeTotalsPerQuorum, nil
55 | }
56 |
--------------------------------------------------------------------------------
/core/chainio/bindings.go:
--------------------------------------------------------------------------------
1 | package chainio
2 |
3 | import (
4 | "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth"
5 | "github.com/Layr-Labs/eigensdk-go/logging"
6 |
7 | "github.com/ethereum/go-ethereum/accounts/abi/bind"
8 | gethcommon "github.com/ethereum/go-ethereum/common"
9 |
10 | csservicemanager "github.com/AvaProtocol/EigenLayer-AVS/contracts/bindings/AutomationServiceManager"
11 | cstaskmanager "github.com/AvaProtocol/EigenLayer-AVS/contracts/bindings/AutomationTaskManager"
12 | regcoord "github.com/Layr-Labs/eigensdk-go/contracts/bindings/RegistryCoordinator"
13 | )
14 |
15 | type AvsManagersBindings struct {
16 | TaskManager *cstaskmanager.ContractAutomationTaskManager
17 | ServiceManager *csservicemanager.ContractAutomationServiceManager
18 | ethClient eth.HttpBackend
19 | logger logging.Logger
20 | }
21 |
22 | func NewAvsManagersBindings(registryCoordinatorAddr, operatorStateRetrieverAddr gethcommon.Address, ethclient eth.HttpBackend, logger logging.Logger) (*AvsManagersBindings, error) {
23 | contractRegistryCoordinator, err := regcoord.NewContractRegistryCoordinator(registryCoordinatorAddr, ethclient)
24 | if err != nil {
25 | return nil, err
26 | }
27 | serviceManagerAddr, err := contractRegistryCoordinator.ServiceManager(&bind.CallOpts{})
28 | if err != nil {
29 | return nil, err
30 | }
31 | contractServiceManager, err := csservicemanager.NewContractAutomationServiceManager(serviceManagerAddr, ethclient)
32 | if err != nil {
33 | logger.Error("Failed to fetch IServiceManager contract", "err", err)
34 | return nil, err
35 | }
36 |
37 | taskManagerAddr, err := contractServiceManager.AutomationTaskManager(&bind.CallOpts{})
38 | if err != nil {
39 | logger.Error("Failed to fetch TaskManager address", "err", err)
40 | return nil, err
41 | }
42 | contractTaskManager, err := cstaskmanager.NewContractAutomationTaskManager(taskManagerAddr, ethclient)
43 | if err != nil {
44 | logger.Error("Failed to fetch IAutomationTaskManager contract", "err", err)
45 | return nil, err
46 | }
47 |
48 | return &AvsManagersBindings{
49 | ServiceManager: contractServiceManager,
50 | TaskManager: contractTaskManager,
51 | ethClient: ethclient,
52 | logger: logger,
53 | }, nil
54 | }
55 |
--------------------------------------------------------------------------------
/core/chainio/signer/bls.go:
--------------------------------------------------------------------------------
1 | package signer
2 |
3 | import (
4 | "github.com/Layr-Labs/eigensdk-go/crypto/bls"
5 | "golang.org/x/crypto/sha3"
6 | )
7 |
8 | // Generate a byte32 fixed hash for a message to be use in various place for
9 | // signature. Many algorithm require a fixed 32 byte input
10 | func Byte32Digest(data []byte) ([32]byte, error) {
11 | var digest [32]byte
12 | hasher := sha3.NewLegacyKeccak256()
13 | hasher.Write(data)
14 | copy(data[:], hasher.Sum(nil)[:32])
15 |
16 | return digest, nil
17 | }
18 |
19 | func SignBlsMessage(blsKeypair *bls.KeyPair, msg []byte) *bls.Signature {
20 | data, e := Byte32Digest(msg)
21 | if e != nil {
22 | return nil
23 | }
24 |
25 | return blsKeypair.SignMessage(data)
26 | }
27 |
--------------------------------------------------------------------------------
/core/chainio/signer/signer.go:
--------------------------------------------------------------------------------
1 | package signer
2 |
3 | import (
4 | "crypto/ecdsa"
5 | "fmt"
6 | "math/big"
7 | "strings"
8 |
9 | "github.com/ethereum/go-ethereum/accounts"
10 | "github.com/ethereum/go-ethereum/accounts/abi/bind"
11 | "github.com/ethereum/go-ethereum/common"
12 | "github.com/ethereum/go-ethereum/common/hexutil"
13 | "github.com/ethereum/go-ethereum/crypto"
14 | )
15 |
16 | const (
17 | eip191Prefix = "\x19Ethereum Signed Message:\n"
18 | )
19 |
20 | func FromPrivateKeyHex(privateKeyHex string, chainID *big.Int) (*bind.TransactOpts, error) {
21 | if strings.HasPrefix(privateKeyHex, "0x") {
22 | privateKeyHex = privateKeyHex[2:]
23 | }
24 | privateKey, err := crypto.HexToECDSA(privateKeyHex)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | return bind.NewKeyedTransactorWithChainID(privateKey, chainID)
30 | }
31 |
32 | // Generate EIP191 signature
33 | func SignMessage(key *ecdsa.PrivateKey, data []byte) ([]byte, error) {
34 | prefix := []byte(eip191Prefix + fmt.Sprint(len(data)))
35 | prefixedData := append(prefix, data...)
36 | hash := crypto.Keccak256Hash(prefixedData)
37 | sig, e := crypto.Sign(hash.Bytes(), key)
38 | // https://stackoverflow.com/questions/69762108/implementing-ethereum-personal-sign-eip-191-from-go-ethereum-gives-different-s
39 | sig[64] += 27
40 |
41 | return sig, e
42 | }
43 |
44 | // Generate EIP191 signature but return a string
45 | func SignMessageAsHex(key *ecdsa.PrivateKey, data []byte) (string, error) {
46 | signature, e := SignMessage(key, data)
47 | if e == nil {
48 | return common.Bytes2Hex(signature), nil
49 | }
50 |
51 | return "", e
52 | }
53 |
54 | // Verify takes an original message, a signature and an address and return true
55 | // or false whether the signature is indeed signed by the address
56 | func Verify(text []byte, sig string, submitAddress string) (bool, error) {
57 | hash := accounts.TextHash(text)
58 |
59 | if sig[0:2] != "0x" {
60 | sig = "0x" + sig
61 | }
62 | signature, err := hexutil.Decode(sig)
63 | if err != nil {
64 | return false, err
65 | }
66 | // https://stackoverflow.com/questions/49085737/geth-ecrecover-invalid-signature-recovery-id
67 | if signature[crypto.RecoveryIDOffset] == 27 || signature[crypto.RecoveryIDOffset] == 28 {
68 | signature[crypto.RecoveryIDOffset] -= 27 // Transform yellow paper V from 27/28 to 0/1
69 | }
70 |
71 | sigPublicKey, err := crypto.SigToPub(hash, signature)
72 | recoveredAddr := crypto.PubkeyToAddress(*sigPublicKey)
73 | if err != nil {
74 | return false, err
75 | }
76 |
77 | if !strings.EqualFold(submitAddress, recoveredAddr.String()) {
78 | return false, fmt.Errorf("Invalid signature")
79 | }
80 |
81 | return true, nil
82 | }
83 |
--------------------------------------------------------------------------------
/core/config/envs.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import "math/big"
4 |
5 | type ChainEnv string
6 |
7 | const (
8 | HoleskyEnv = ChainEnv("holesky")
9 | EthereumEnv = ChainEnv("ethereum")
10 | )
11 |
12 | var (
13 | MainnetChainID = big.NewInt(1)
14 | CurrentChainEnv = ChainEnv("ethereum")
15 |
16 | etherscanURLs = map[ChainEnv]string{
17 | EthereumEnv: "https://etherscan.io",
18 | HoleskyEnv: "https://holesky.etherscan.io",
19 | }
20 |
21 | eigenlayerAppURLs = map[ChainEnv]string{
22 | EthereumEnv: "https://app.eigenlayer.xyz",
23 | HoleskyEnv: "https://holesky.eigenlayer.xyz",
24 | }
25 | )
26 |
27 | func EtherscanURL() string {
28 | if url, ok := etherscanURLs[CurrentChainEnv]; ok {
29 | return url
30 | }
31 |
32 | return "https://etherscan.io"
33 | }
34 |
35 | func EigenlayerAppURL() string {
36 | if url, ok := eigenlayerAppURLs[CurrentChainEnv]; ok {
37 | return url
38 | }
39 |
40 | return "https://eigenlayer.xyz"
41 | }
42 |
--------------------------------------------------------------------------------
/core/config/helpers.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/ethereum/go-ethereum/common"
5 | )
6 |
7 | func convertToAddressSlice(addresses []string) []common.Address {
8 | result := make([]common.Address, len(addresses))
9 | for i, addr := range addresses {
10 | result[i] = common.HexToAddress(addr)
11 | }
12 | return result
13 | }
14 |
--------------------------------------------------------------------------------
/core/migrator/migrator.go:
--------------------------------------------------------------------------------
1 | package migrator
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/AvaProtocol/EigenLayer-AVS/core/backup"
7 | "github.com/AvaProtocol/EigenLayer-AVS/storage"
8 | )
9 |
10 | import (
11 | "fmt"
12 | "log"
13 | "sync"
14 | )
15 |
16 | // MigrationFunc is a function that performs a database migration. The migration functions need to follow this signature
17 | // and return the number of records updated and an error if the migration fails
18 | type MigrationFunc func(db storage.Storage) (int, error)
19 |
20 | // Migration represents a database migration function
21 | type Migration struct {
22 | Name string
23 | Function MigrationFunc
24 | }
25 |
26 | // Migrator handles database migrations
27 | type Migrator struct {
28 | db storage.Storage
29 | migrations []Migration
30 | backup *backup.Service
31 | mu sync.Mutex
32 | }
33 |
34 | // NewMigrator creates a new migrator instance
35 | func NewMigrator(db storage.Storage, backup *backup.Service, migrations []Migration) *Migrator {
36 | return &Migrator{
37 | db: db,
38 | migrations: migrations,
39 | backup: backup,
40 | }
41 | }
42 |
43 | // Register adds a new migration to the list
44 | func (m *Migrator) Register(name string, fn MigrationFunc) {
45 | m.mu.Lock()
46 | defer m.mu.Unlock()
47 |
48 | m.migrations = append(m.migrations, Migration{
49 | Name: name,
50 | Function: fn,
51 | })
52 | }
53 |
54 | // Run executes all registered migrations that haven't been run yet
55 | func (m *Migrator) Run() error {
56 | m.mu.Lock()
57 | defer m.mu.Unlock()
58 |
59 | // Check if we have any migrations to run
60 | hasPendingMigrations := false
61 | for _, migration := range m.migrations {
62 | key := fmt.Sprintf("migration:%s", migration.Name)
63 | exists, err := m.db.Exist([]byte(key))
64 | if err != nil || !exists {
65 | hasPendingMigrations = true
66 | break
67 | }
68 | }
69 |
70 | // If we have migrations to run, take a backup first
71 | if hasPendingMigrations {
72 | log.Printf("Pending migrations found, creating database backup before proceeding")
73 | if backupFile, err := m.backup.PerformBackup(); err != nil {
74 | return fmt.Errorf("failed to create backup before migrations: %w", err)
75 | } else {
76 | log.Printf("Database backup created at %s", backupFile)
77 | }
78 | }
79 |
80 | for _, migration := range m.migrations {
81 | // Check if migration has already been run
82 | key := fmt.Sprintf("migration:%s", migration.Name)
83 | exists, err := m.db.Exist([]byte(key))
84 | if exists && err == nil {
85 | log.Printf("Migration %s already applied, skipping", migration.Name)
86 | continue
87 | }
88 |
89 | // Run the migration
90 | log.Printf("Running migration: %s", migration.Name)
91 | recordsUpdated, err := migration.Function(m.db)
92 | if err != nil {
93 | return fmt.Errorf("migration %s failed: %w", migration.Name, err)
94 | } else {
95 | log.Printf("Migration %s completed successfully. %d records updated.", migration.Name, recordsUpdated)
96 | }
97 |
98 | // Mark migration as complete in the database
99 | if err := m.db.Set([]byte(key), []byte(fmt.Sprintf("records=%d,ts=%d", recordsUpdated, time.Now().UnixMilli()))); err != nil {
100 | return fmt.Errorf("failed to mark migration as complete in database: %w", err)
101 | }
102 | }
103 |
104 | return nil
105 | }
106 |
--------------------------------------------------------------------------------
/core/migrator/migrator_test.go:
--------------------------------------------------------------------------------
1 | package migrator
2 |
3 | import (
4 | "strings"
5 | "testing"
6 |
7 | "github.com/AvaProtocol/EigenLayer-AVS/core/backup"
8 | "github.com/AvaProtocol/EigenLayer-AVS/core/testutil"
9 | "github.com/AvaProtocol/EigenLayer-AVS/storage"
10 | )
11 |
12 | func TestMigrator(t *testing.T) {
13 | // Setup test database
14 | logger := testutil.GetLogger()
15 | db := testutil.TestMustDB()
16 | defer db.Close()
17 |
18 | // Create backup service
19 | backupDir := t.TempDir()
20 | backup := backup.NewService(logger, db, backupDir)
21 |
22 | // Test migration function that updates records
23 | testMigration := func(db storage.Storage) (int, error) {
24 | return 5, db.Set([]byte("test:key"), []byte("migrated"))
25 | }
26 |
27 | // Create migrator with test migration
28 | migrations := []Migration{} // Initialize with empty slice
29 | migrator := NewMigrator(db, backup, migrations)
30 | migrator.Register("test_migration", testMigration)
31 |
32 | // Run migrations
33 | err := migrator.Run()
34 | if err != nil {
35 | t.Fatalf("Failed to run migrations: %v", err)
36 | }
37 |
38 | // Verify migration was marked as complete
39 | migrationKey := []byte("migration:test_migration")
40 | exists, err := db.Exist(migrationKey)
41 | if err != nil {
42 | t.Fatalf("Failed to check if migration exists: %v", err)
43 | }
44 | if !exists {
45 | t.Fatalf("Migration was not marked as complete")
46 | }
47 |
48 | // Verify migration record format (should contain records count and timestamp)
49 | migrationData, err := db.GetKey(migrationKey)
50 | if err != nil {
51 | t.Fatalf("Failed to get migration data: %v", err)
52 | }
53 |
54 | migrationRecord := string(migrationData)
55 | if !strings.Contains(migrationRecord, "records=5") {
56 | t.Errorf("Migration record doesn't contain correct record count: %s", migrationRecord)
57 | }
58 | if !strings.Contains(migrationRecord, "ts=") {
59 | t.Errorf("Migration record doesn't contain timestamp: %s", migrationRecord)
60 | }
61 |
62 | // Test that migrations aren't run twice
63 | // Create a counter to track if migration is called
64 | migrationCounter := 0
65 | countingMigration := func(db storage.Storage) (int, error) {
66 | migrationCounter++
67 | return 0, nil
68 | }
69 |
70 | // Register a new migration that we've already run
71 | migrator.Register("test_migration", countingMigration)
72 |
73 | // Run migrations again
74 | err = migrator.Run()
75 | if err != nil {
76 | t.Fatalf("Failed to run migrations second time: %v", err)
77 | }
78 |
79 | // Verify the migration wasn't executed again
80 | if migrationCounter > 0 {
81 | t.Errorf("Migration was executed again when it should have been skipped")
82 | }
83 |
84 | // Test new migration gets executed
85 | migrator.Register("second_migration", countingMigration)
86 |
87 | // Run migrations again
88 | err = migrator.Run()
89 | if err != nil {
90 | t.Fatalf("Failed to run migrations third time: %v", err)
91 | }
92 |
93 | // Verify the new migration was executed
94 | if migrationCounter != 1 {
95 | t.Errorf("New migration was not executed")
96 | }
97 |
98 | // Verify second migration was marked as complete
99 | secondMigrationKey := []byte("migration:second_migration")
100 | exists, err = db.Exist(secondMigrationKey)
101 | if err != nil {
102 | t.Fatalf("Failed to check if second migration exists: %v", err)
103 | }
104 | if !exists {
105 | t.Fatalf("Second migration was not marked as complete")
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/core/taskengine/cursor.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/json"
6 | "strconv"
7 |
8 | ulid "github.com/oklog/ulid/v2"
9 | )
10 |
11 | type CursorDirection string
12 |
13 | const (
14 | CursorDirectionNext = CursorDirection("next")
15 | CursorDirectionPrevious = CursorDirection("prev")
16 | )
17 |
18 | type Cursor struct {
19 | Direction CursorDirection `json:"d"`
20 | Position string `json:"p"`
21 |
22 | parsePos bool `json:"-"`
23 | int64Pos int64 `json:"-"`
24 | ulidPos ulid.ULID `json:"-"`
25 | }
26 |
27 | func CursorFromString(data string) (*Cursor, error) {
28 | c := &Cursor{
29 | Direction: CursorDirectionNext,
30 | Position: "0",
31 | parsePos: false,
32 | int64Pos: 0,
33 | ulidPos: ulid.Zero,
34 | }
35 |
36 | if data == "" {
37 | return c, nil
38 | }
39 |
40 | decoded, err := base64.StdEncoding.DecodeString(data)
41 | if err != nil {
42 | return c, err
43 | }
44 |
45 | if err = json.Unmarshal(decoded, &c); err == nil {
46 | return c, nil
47 | } else {
48 | return c, err
49 | }
50 | }
51 |
52 | func CursorFromBeforeAfter(before, after string) (*Cursor, error) {
53 | if after != "" {
54 | cursor, err := CursorFromString(after)
55 | if err != nil {
56 | return nil, err
57 | }
58 | cursor.Direction = CursorDirectionNext
59 | return cursor, nil
60 | }
61 |
62 | if before != "" {
63 | cursor, err := CursorFromString(before)
64 | if err != nil {
65 | return nil, err
66 | }
67 | cursor.Direction = CursorDirectionPrevious
68 | return cursor, nil
69 | }
70 |
71 | return &Cursor{
72 | Direction: CursorDirectionNext,
73 | Position: "0",
74 | parsePos: false,
75 | int64Pos: 0,
76 | ulidPos: ulid.Zero,
77 | }, nil
78 | }
79 |
80 | func NewCursor(direction CursorDirection, position string) *Cursor {
81 | return &Cursor{
82 | Direction: direction,
83 | Position: position,
84 |
85 | parsePos: false,
86 | int64Pos: 0,
87 | }
88 | }
89 |
90 | func (c *Cursor) IsZero() bool {
91 | return c.Position == "0"
92 | }
93 |
94 | func (c *Cursor) String() string {
95 | var d []byte
96 | d, err := json.Marshal(c)
97 |
98 | if err != nil {
99 | return ""
100 | }
101 |
102 | encoded := base64.StdEncoding.EncodeToString(d)
103 |
104 | return encoded
105 | }
106 |
107 | // Given a value, return true if the value is after the cursor
108 | func (c *Cursor) LessThanInt64(value int64) bool {
109 | if !c.parsePos {
110 | c.int64Pos, _ = strconv.ParseInt(c.Position, 10, 64)
111 | c.parsePos = true
112 | }
113 | if c.Direction == CursorDirectionNext {
114 | return c.int64Pos <= value
115 | }
116 |
117 | return c.int64Pos >= value
118 | }
119 |
120 | // Given a value, return true if the value is after the cursor
121 | func (c *Cursor) LessThanUlid(value ulid.ULID) bool {
122 | if !c.parsePos {
123 | var err error
124 | c.ulidPos, err = ulid.Parse(c.Position)
125 | if err != nil {
126 | c.ulidPos = ulid.Zero
127 | }
128 | c.parsePos = true
129 | }
130 | if c.Direction == CursorDirectionNext {
131 | return c.ulidPos.Compare(value) < 0
132 | }
133 | return c.ulidPos.Compare(value) > 0
134 | }
135 |
136 | // Given a value, return true if the value is after the cursor
137 | func (c *Cursor) LessThanOrEqualInt64(value int64) bool {
138 | if !c.parsePos {
139 | c.int64Pos, _ = strconv.ParseInt(c.Position, 10, 64)
140 | c.parsePos = true
141 | }
142 | if c.Direction == CursorDirectionNext {
143 | return c.int64Pos <= value
144 | }
145 |
146 | return c.int64Pos >= value
147 | }
148 |
149 | // Given a value, return true if the value is after the cursor
150 | func (c *Cursor) LessThanOrEqualUlid(value ulid.ULID) bool {
151 | if !c.parsePos {
152 | var err error
153 | c.ulidPos, err = ulid.Parse(c.Position)
154 | if err != nil {
155 | c.ulidPos = ulid.Zero
156 | }
157 | c.parsePos = true
158 | }
159 | if c.Direction == CursorDirectionNext {
160 | return c.ulidPos.Compare(value) <= 0
161 | }
162 | return c.ulidPos.Compare(value) >= 0
163 | }
164 |
--------------------------------------------------------------------------------
/core/taskengine/custom_code_language_test.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "testing"
5 |
6 | avsproto "github.com/AvaProtocol/EigenLayer-AVS/protobuf"
7 | )
8 |
9 | func TestCustomCodeLanguageConversion(t *testing.T) {
10 | // Test what the backend sees when language enum is set
11 | config := &avsproto.CustomCodeNode_Config{
12 | Lang: avsproto.Lang_JavaScript, // This is the enum value 0
13 | Source: "console.log('test');",
14 | }
15 |
16 | // Test what String() method returns
17 | langStr := config.Lang.String()
18 | t.Logf("Protobuf Lang.String() returns: '%s'", langStr)
19 |
20 | // This is what the JSProcessor expects to see
21 | if langStr != "JavaScript" {
22 | t.Errorf("Expected 'JavaScript', got '%s'", langStr)
23 | }
24 |
25 | // Test with CustomCodeNode to simulate real usage
26 | node := &avsproto.CustomCodeNode{
27 | Config: config,
28 | }
29 |
30 | // Simulate what JSProcessor.Execute() does
31 | if node.Config != nil {
32 | actualLangStr := node.Config.Lang.String()
33 | t.Logf("JSProcessor sees language as: '%s'", actualLangStr)
34 |
35 | // Verify it matches protobuf enum string representation
36 | if actualLangStr != "JavaScript" {
37 | t.Errorf("JSProcessor should see 'JavaScript', but sees '%s'", actualLangStr)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/core/taskengine/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Task Engine handles task storage and execution. We use badgerdb for all of our task storage. We like to make sure of Go cross compiling extensively and want to leverage pure-go as much as possible. badgerdb sastify that requirement.
3 |
4 | In KV store, we want to use short key to save space. The key is what loaded into RAM, the smaller the better. It's also helpful for key only scan.
5 |
6 | **Wallet Info**
7 |
8 | w:: = {factory_address: address, salt: salt}
9 |
10 | **Task Storage**
11 |
12 | w:: -> {factory, salt}
13 | t:: -> task payload, the source of truth of task information
14 | u::: -> task status
15 | history:: -> an execution history
16 | trigger:: -> execution status
17 | ct:cw: -> counter value -> track contract write by eoa
18 |
19 | The task storage was designed for fast retrieve time at the cost of extra storage.
20 |
21 | The storage can also be easily back-up, sync due to simplicity of supported write operation.
22 |
23 | **Data console**
24 |
25 | Storage can also be inspect with telnet:
26 |
27 | telnet /tmp/ap.sock
28 |
29 | Then issue `get ` or `list ` or `list *` to inspect current keys in the storage.
30 |
31 | **Secret Storage**
32 | - currently org_id will always be _ because we haven't implemented it yet
33 | - workflow_id will also be _ so we can search be prefix
34 | secret:::: -> value
35 | */
36 | package taskengine
37 |
--------------------------------------------------------------------------------
/core/taskengine/errors.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | const (
4 | InternalError = "internal error"
5 | TaskNotFoundError = "task not found"
6 | ExecutionNotFoundError = "execution not found"
7 |
8 | InvalidSmartAccountAddressError = "invalid smart account address"
9 | InvalidFactoryAddressError = "invalid factory address"
10 | InvalidSmartAccountSaltError = "invalid salt value"
11 | InvalidTaskIdFormat = "invalid task id"
12 | SmartAccountCreationError = "cannot determine smart wallet address"
13 | NonceFetchingError = "cannot determine nonce for smart wallet"
14 |
15 | MissingSmartWalletAddressError = "Missing smart_wallet_address"
16 |
17 | StorageUnavailableError = "storage is not ready"
18 | StorageWriteError = "cannot write to storage"
19 | StorageQueueUnavailableError = "queue storage system is not ready"
20 |
21 | TaskStorageCorruptedError = "task data storage is corrupted"
22 | TaskIDMissing = "Missing task id in request"
23 | TaskIsNotRunnable = "task cannot be executed: either reached max execution, expired, or not yet started"
24 |
25 | InvalidCursor = "cursor is not valid"
26 | InvalidPaginationParam = "item per page is not valid"
27 |
28 | InvalidEntrypoint = "cannot detect task entrypoint from trigger and edge data"
29 | )
30 |
--------------------------------------------------------------------------------
/core/taskengine/executor_nil_reason_test.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "testing"
7 |
8 | "github.com/AvaProtocol/EigenLayer-AVS/core/testutil"
9 | "github.com/AvaProtocol/EigenLayer-AVS/model"
10 | avsproto "github.com/AvaProtocol/EigenLayer-AVS/protobuf"
11 | "github.com/AvaProtocol/EigenLayer-AVS/storage"
12 | )
13 |
14 | func TestExecutorRunTaskWithNilReason(t *testing.T) {
15 | SetRpc(testutil.GetTestRPCURL())
16 | SetCache(testutil.GetDefaultCache())
17 | db := testutil.TestMustDB()
18 | defer storage.Destroy(db.(*storage.BadgerStorage))
19 |
20 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
21 | w.WriteHeader(http.StatusOK)
22 | w.Write([]byte(`{"message": "success"}`))
23 | }))
24 | defer server.Close()
25 |
26 | nodes := []*avsproto.TaskNode{
27 | {
28 | Id: "rest1",
29 | Name: "httpnode",
30 | TaskType: &avsproto.TaskNode_RestApi{
31 | RestApi: &avsproto.RestAPINode{
32 | Config: &avsproto.RestAPINode_Config{
33 | Url: server.URL,
34 | Method: "POST",
35 | Body: `{"test": "data"}`,
36 | },
37 | },
38 | },
39 | },
40 | }
41 |
42 | trigger := &avsproto.TaskTrigger{
43 | Id: "manual_trigger",
44 | Name: "manual_trigger",
45 | }
46 | edges := []*avsproto.TaskEdge{
47 | {
48 | Id: "e1",
49 | Source: trigger.Id,
50 | Target: "rest1",
51 | },
52 | }
53 |
54 | task := &model.Task{
55 | Task: &avsproto.Task{
56 | Id: "ManualTaskID",
57 | Nodes: nodes,
58 | Edges: edges,
59 | Trigger: trigger,
60 | },
61 | }
62 |
63 | executor := NewExecutor(testutil.GetTestSmartWalletConfig(), db, testutil.GetLogger())
64 | execution, err := executor.RunTask(task, &QueueExecutionData{
65 | TriggerType: avsproto.TriggerType_TRIGGER_TYPE_UNSPECIFIED,
66 | TriggerOutput: nil,
67 | ExecutionID: "exec_nil_reason",
68 | })
69 |
70 | if err != nil {
71 | t.Errorf("Expected no error with unspecified trigger type, but got: %v", err)
72 | }
73 |
74 | if !execution.Success {
75 | t.Errorf("Expected success status but got failure")
76 | }
77 |
78 | if len(execution.Steps) != 2 {
79 | t.Errorf("Expected 2 steps (trigger + node) but got: %d", len(execution.Steps))
80 | }
81 |
82 | // Check node step at index 0 (no trigger step in regular executions)
83 | if execution.Steps[1].Id != "rest1" {
84 | t.Errorf("Expected step ID to be rest1 but got: %s", execution.Steps[1].Id)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/core/taskengine/macros/abi_const.go:
--------------------------------------------------------------------------------
1 | package macros
2 |
3 | import _ "embed"
4 |
5 | //go:embed abis/chainlink/eac_aggregator_proxy.json
6 | var chainlinkABI string
7 |
--------------------------------------------------------------------------------
/core/taskengine/macros/contract.go:
--------------------------------------------------------------------------------
1 | package macros
2 |
3 | import (
4 | "context"
5 | "strings"
6 |
7 | ethereum "github.com/ethereum/go-ethereum"
8 | "github.com/ethereum/go-ethereum/accounts/abi"
9 | "github.com/ethereum/go-ethereum/common"
10 | "github.com/ethereum/go-ethereum/ethclient"
11 | )
12 |
13 | // QueryContractRaw
14 | func QueryContractRaw(
15 | ctx context.Context,
16 | client *ethclient.Client,
17 | contractAddress common.Address,
18 | data []byte,
19 | ) ([]byte, error) {
20 | // Prepare the call message
21 | msg := ethereum.CallMsg{
22 | To: &contractAddress,
23 | Data: data,
24 | }
25 |
26 | return client.CallContract(ctx, msg, nil)
27 | }
28 |
29 | // QueryContract
30 | func QueryContract(
31 | client *ethclient.Client,
32 | contractAddress common.Address,
33 | contractABI string,
34 | method string,
35 | inputs ...any,
36 | ) ([]interface{}, error) {
37 | // Parse the ABI
38 | parsedABI, err := abi.JSON(strings.NewReader(contractABI))
39 | if err != nil {
40 | return nil, err
41 | }
42 |
43 | data, e := parsedABI.Pack(method, inputs...)
44 | if e != nil {
45 | return nil, e
46 | }
47 |
48 | // Perform the call
49 | output, err := QueryContractRaw(context.Background(), client, contractAddress, data)
50 | if err != nil {
51 | return nil, err
52 | }
53 |
54 | // Unpack the output
55 | return parsedABI.Unpack(method, output)
56 | }
57 |
--------------------------------------------------------------------------------
/core/taskengine/macros/contract_test.go:
--------------------------------------------------------------------------------
1 | package macros
2 |
3 | import (
4 | "math/big"
5 | "os"
6 | "testing"
7 |
8 | "github.com/ethereum/go-ethereum/common"
9 | "github.com/ethereum/go-ethereum/ethclient"
10 | "github.com/expr-lang/expr"
11 | )
12 |
13 | type answer struct {
14 | RoundId big.Int
15 | Answer big.Int
16 | StartedAt uint64
17 | UpdatedAt uint64
18 | AnsweredInRound big.Int
19 | }
20 |
21 | func TestQueryContract(t *testing.T) {
22 | conn, _ := ethclient.Dial(os.Getenv("RPC_URL"))
23 |
24 | r, err := QueryContract(
25 | conn,
26 | // https://docs.chain.link/data-feeds/price-feeds/addresses?network=ethereum&page=1&search=et#sepolia-testnet
27 | // ETH-USD pair on sepolia
28 | common.HexToAddress("0x694AA1769357215DE4FAC081bf1f309aDC325306"),
29 | chainlinkABI,
30 | "latestRoundData",
31 | )
32 |
33 | if err != nil {
34 | t.Errorf("contract query error: %v", err)
35 | }
36 |
37 | t.Logf("contract query result: %v", r)
38 | }
39 |
40 | func TestExpression(t *testing.T) {
41 | SetRpc(os.Getenv("RPC_URL"))
42 |
43 | p, e := CompileExpression(`priceChainlink("0x694AA1769357215DE4FAC081bf1f309aDC325306")`)
44 | if e != nil {
45 | t.Errorf("Compile expression error: %v", e)
46 | }
47 |
48 | r, e := expr.Run(p, exprEnv)
49 | if e != nil {
50 | t.Errorf("Run expr error: %v %v", e, r)
51 | }
52 |
53 | if r.(*big.Int).Cmp(big.NewInt(10)) <= 0 {
54 | t.Errorf("Invalid result data: %v", r)
55 | }
56 |
57 | t.Logf("Exp Run Result: %v", r.(*big.Int))
58 |
59 | match, e := RunExpressionQuery(`
60 | bigCmp(
61 | priceChainlink("0x694AA1769357215DE4FAC081bf1f309aDC325306"),
62 | toBigInt("2000")
63 | ) > 0
64 | `)
65 | if e != nil {
66 | t.Errorf("Run expr error: %v %v", e, r)
67 | }
68 | if !match {
69 | t.Error("Evaluate error. Expected: true, received: false")
70 | }
71 |
72 | match, e = RunExpressionQuery(`
73 | bigCmp(
74 | priceChainlink("0x694AA1769357215DE4FAC081bf1f309aDC325306"),
75 | toBigInt("9262391230023")
76 | ) > 0
77 | `)
78 | if e != nil {
79 | t.Errorf("Run expr error: %v %v", e, r)
80 | }
81 | if match {
82 | t.Error("Evaluate error. Expected: false, got: true")
83 | }
84 | }
85 |
86 | func TestExpressionDynamic(t *testing.T) {
87 | SetRpc(os.Getenv("RPC_URL"))
88 |
89 | // https://sepolia.etherscan.io/address/0x9aCb42Ac07C72cFc29Cd95d9DEaC807E93ada1F6#code
90 | match, e := RunExpressionQuery(`
91 | bigCmp(
92 | readContractData(
93 | "0x9aCb42Ac07C72cFc29Cd95d9DEaC807E93ada1F6",
94 | "0x0a79309b000000000000000000000000e0f7d11fd714674722d325cd86062a5f1882e13a",
95 | "retrieve",
96 | '[{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"retrieve","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]'
97 | )[0],
98 | toBigInt("2000")
99 | ) > 0
100 | `)
101 | if e != nil {
102 | t.Errorf("Run expr error: %v %v", e, match)
103 | }
104 | if !match {
105 | t.Error("Evaluate error. Expected: true, received: false")
106 | }
107 | }
108 |
109 | func TestExpressionPanicWonCrash(t *testing.T) {
110 | rpcConn = nil
111 | p, e := CompileExpression(`priceChainlink("0x694AA1769357215DE4FAC081bf1f309aDC325306")`)
112 | if e != nil {
113 | t.Errorf("Compile expression error: %v", e)
114 | }
115 |
116 | r, e := expr.Run(p, exprEnv)
117 | if e == nil || r != nil {
118 | t.Errorf("Evaluate wrong. Expected: nil, got: %v", r)
119 | }
120 |
121 | t.Logf("Successfully recovered from VM crash")
122 | }
123 |
--------------------------------------------------------------------------------
/core/taskengine/macros/exp_test.go:
--------------------------------------------------------------------------------
1 | package macros
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestChainlinkLatestAnswer(t *testing.T) {
8 | SetRpc("https://sepolia.drpc.org")
9 |
10 | // https://sepolia.etherscan.io/address/0x9aCb42Ac07C72cFc29Cd95d9DEaC807E93ada1F6#code
11 | value := chainlinkLatestAnswer("0x694AA1769357215DE4FAC081bf1f309aDC325306")
12 | if value == nil {
13 | t.Errorf("fail to query chainlink answer. expect a value, got nil")
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/core/taskengine/macros/goja_utils_test.go:
--------------------------------------------------------------------------------
1 | package macros
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/dop251/goja"
7 | )
8 |
9 | func TestObjectToString(t *testing.T) {
10 | runtime := goja.New()
11 |
12 | ConfigureGojaRuntime(runtime)
13 |
14 | result, err := runtime.RunString("({}).toString()")
15 | if err != nil {
16 | t.Errorf("Failed to run script: %v", err)
17 | }
18 |
19 | if result.String() != "[object Object]" {
20 | t.Errorf("Expected '[object Object]' but got '%s'", result.String())
21 | }
22 |
23 | result, err = runtime.RunString("'Result: ' + {}")
24 | if err != nil {
25 | t.Errorf("Failed to run script: %v", err)
26 | }
27 |
28 | if result.String() != "Result: [object Object]" {
29 | t.Errorf("Expected 'Result: [object Object]' but got '%s'", result.String())
30 | }
31 |
32 | result, err = runtime.RunString("'Value: ' + {a: {b: 1}}")
33 | if err != nil {
34 | t.Errorf("Failed to run script: %v", err)
35 | }
36 |
37 | if result.String() != "Value: [object Object]" {
38 | t.Errorf("Expected 'Value: [object Object]' but got '%s'", result.String())
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/core/taskengine/modules/builtins.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import (
4 | "embed"
5 | "errors"
6 | "fmt"
7 | "io/fs"
8 |
9 | "github.com/dop251/goja"
10 | )
11 |
12 | //go:embed libs/*
13 | var builtinLibs embed.FS
14 |
15 | type BuiltinLoader struct {
16 | libraries map[string][]byte
17 | }
18 |
19 | func NewBuiltinLoader() *BuiltinLoader {
20 | return &BuiltinLoader{
21 | libraries: make(map[string][]byte),
22 | }
23 | }
24 |
25 | func (l *BuiltinLoader) RegisterBuiltinLibraries() error {
26 | libMap := map[string]string{
27 | "lodash": "libs/lodash.min.js",
28 | "dayjs": "libs/dayjs.min.js",
29 | "uuid": "libs/uuid.min.js",
30 | }
31 |
32 | for name, path := range libMap {
33 | content, err := fs.ReadFile(builtinLibs, path)
34 | if err != nil {
35 | return err
36 | }
37 | l.libraries[name] = content
38 | }
39 |
40 | return nil
41 | }
42 |
43 | func (l *BuiltinLoader) Load(runtime *goja.Runtime, name string) (goja.Value, error) {
44 | content, ok := l.libraries[name]
45 | if !ok {
46 | return nil, errors.New("module not found: " + name)
47 | }
48 |
49 | // Create module and exports objects
50 | module := runtime.NewObject()
51 | exports := runtime.NewObject()
52 | module.Set("exports", exports)
53 |
54 | // Set the module and exports objects in the runtime
55 | runtime.Set("module", module)
56 | runtime.Set("exports", exports)
57 |
58 | // Special handling for Day.js and UUID - they're designed to work well with module systems
59 | if name == "dayjs" || name == "uuid" {
60 | // These libraries have better UMD support, so we can run them more simply
61 | moduleScript := fmt.Sprintf(`
62 | (function(module, exports) {
63 | %s
64 | return module.exports;
65 | })(module, exports);
66 | `, string(content))
67 |
68 | result, err := runtime.RunString(moduleScript)
69 | if err != nil {
70 | return nil, fmt.Errorf("error loading module %s: %v", name, err)
71 | }
72 | return result, nil
73 | }
74 |
75 | // Wrap the code so that 'this' is the global object
76 | moduleScript := fmt.Sprintf(`
77 | (function(module, exports) {
78 | (function() {
79 | %s
80 | }).call(this);
81 | return module.exports || exports;
82 | })(module, exports);
83 | `, string(content))
84 |
85 | result, err := runtime.RunString(moduleScript)
86 | if err != nil {
87 | return nil, fmt.Errorf("error loading module %s: %v", name, err)
88 | }
89 |
90 | return result, nil
91 | }
92 |
--------------------------------------------------------------------------------
/core/taskengine/modules/download-libs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Download JavaScript libraries for embedding
4 | LIBS_DIR="libs"
5 | mkdir -p $LIBS_DIR
6 |
7 | curl -s https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js -o $LIBS_DIR/lodash.min.js
8 | echo "Downloaded lodash.min.js"
9 |
10 | curl -s https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.13/dayjs.min.js -o $LIBS_DIR/dayjs.min.js
11 | echo "Downloaded dayjs.min.js"
12 |
13 | curl -s https://unpkg.com/uuid@8.3.2/dist/umd/uuid.min.js -o $LIBS_DIR/uuid.min.js
14 | echo "Downloaded uuid.min.js"
15 |
16 | echo "All libraries downloaded successfully!"
17 |
--------------------------------------------------------------------------------
/core/taskengine/modules/libs/README.md:
--------------------------------------------------------------------------------
1 | # Built-in JavaScript Libraries
2 |
3 | This directory contains pre-bundled JavaScript libraries that are available for use in CustomCode nodes.
4 |
5 | ## Available Libraries
6 |
7 | - **lodash** - A modern JavaScript utility library delivering modularity, performance & extras
8 | - **dayjs** - 2KB immutable date library with the same modern API as Moment.js (recommended for date operations)
9 | - **uuid** - For the creation of RFC4122 UUIDs
10 |
11 | ## Usage
12 |
13 | These libraries can be imported using either CommonJS or ES6 module syntax in your custom code.
14 |
--------------------------------------------------------------------------------
/core/taskengine/modules/libs/uuid.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | Object.defineProperty(exports, "NIL", {
7 | enumerable: true,
8 | get: function () {
9 | return _nil.default;
10 | }
11 | });
12 | Object.defineProperty(exports, "parse", {
13 | enumerable: true,
14 | get: function () {
15 | return _parse.default;
16 | }
17 | });
18 | Object.defineProperty(exports, "stringify", {
19 | enumerable: true,
20 | get: function () {
21 | return _stringify.default;
22 | }
23 | });
24 | Object.defineProperty(exports, "v1", {
25 | enumerable: true,
26 | get: function () {
27 | return _v.default;
28 | }
29 | });
30 | Object.defineProperty(exports, "v3", {
31 | enumerable: true,
32 | get: function () {
33 | return _v2.default;
34 | }
35 | });
36 | Object.defineProperty(exports, "v4", {
37 | enumerable: true,
38 | get: function () {
39 | return _v3.default;
40 | }
41 | });
42 | Object.defineProperty(exports, "v5", {
43 | enumerable: true,
44 | get: function () {
45 | return _v4.default;
46 | }
47 | });
48 | Object.defineProperty(exports, "validate", {
49 | enumerable: true,
50 | get: function () {
51 | return _validate.default;
52 | }
53 | });
54 | Object.defineProperty(exports, "version", {
55 | enumerable: true,
56 | get: function () {
57 | return _version.default;
58 | }
59 | });
60 |
61 | var _v = _interopRequireDefault(require("./v1.js"));
62 |
63 | var _v2 = _interopRequireDefault(require("./v3.js"));
64 |
65 | var _v3 = _interopRequireDefault(require("./v4.js"));
66 |
67 | var _v4 = _interopRequireDefault(require("./v5.js"));
68 |
69 | var _nil = _interopRequireDefault(require("./nil.js"));
70 |
71 | var _version = _interopRequireDefault(require("./version.js"));
72 |
73 | var _validate = _interopRequireDefault(require("./validate.js"));
74 |
75 | var _stringify = _interopRequireDefault(require("./stringify.js"));
76 |
77 | var _parse = _interopRequireDefault(require("./parse.js"));
78 |
79 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
--------------------------------------------------------------------------------
/core/taskengine/modules/registry.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import (
4 | "errors"
5 | "sync"
6 |
7 | "github.com/dop251/goja"
8 | )
9 |
10 | type ModuleLoader interface {
11 | Load(runtime *goja.Runtime, name string) (goja.Value, error)
12 | }
13 |
14 | type Registry struct {
15 | loaders map[string]ModuleLoader
16 | cache map[string]goja.Value
17 | mu sync.RWMutex
18 | }
19 |
20 | func NewRegistry() *Registry {
21 | return &Registry{
22 | loaders: make(map[string]ModuleLoader),
23 | cache: make(map[string]goja.Value),
24 | }
25 | }
26 |
27 | func (r *Registry) RegisterLoader(name string, loader ModuleLoader) {
28 | r.mu.Lock()
29 | defer r.mu.Unlock()
30 | r.loaders[name] = loader
31 | }
32 |
33 | func (r *Registry) Require(runtime *goja.Runtime, name string) (goja.Value, error) {
34 | r.mu.RLock()
35 | cached, ok := r.cache[name]
36 | r.mu.RUnlock()
37 | if ok {
38 | return cached, nil
39 | }
40 |
41 | r.mu.RLock()
42 | loader, ok := r.loaders[name]
43 | r.mu.RUnlock()
44 | if !ok {
45 | return nil, errors.New("module not found: " + name)
46 | }
47 |
48 | exports, err := loader.Load(runtime, name)
49 | if err != nil {
50 | return nil, err
51 | }
52 |
53 | r.mu.Lock()
54 | r.cache[name] = exports
55 | r.mu.Unlock()
56 |
57 | return exports, nil
58 | }
59 |
60 | func (r *Registry) RequireFunction(runtime *goja.Runtime) func(call goja.FunctionCall) goja.Value {
61 | return func(call goja.FunctionCall) goja.Value {
62 | if len(call.Arguments) < 1 {
63 | panic(runtime.NewTypeError("require: module name must be provided"))
64 | }
65 |
66 | moduleName := call.Arguments[0].String()
67 | exports, err := r.Require(runtime, moduleName)
68 | if err != nil {
69 | panic(runtime.NewTypeError("require: " + err.Error()))
70 | }
71 |
72 | return exports
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/core/taskengine/pagination.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "google.golang.org/grpc/codes"
5 | "google.golang.org/grpc/status"
6 | )
7 |
8 | func SetupPagination(before, after string, limit int64) (*Cursor, int, error) {
9 | cursor, err := CursorFromBeforeAfter(before, after)
10 | if err != nil {
11 | return nil, 0, status.Errorf(codes.InvalidArgument, InvalidCursor)
12 | }
13 |
14 | perPage := int(limit)
15 | if perPage < 0 {
16 | return nil, 0, status.Errorf(codes.InvalidArgument, InvalidPaginationParam)
17 | }
18 | if perPage == 0 {
19 | perPage = DefaultLimit
20 | }
21 |
22 | return cursor, perPage, nil
23 | }
24 |
25 | func CreateNextCursor(position string) string {
26 | if position == "" {
27 | return ""
28 | }
29 |
30 | nextCursor := &Cursor{
31 | Direction: CursorDirectionNext,
32 | Position: position,
33 | }
34 | return nextCursor.String()
35 | }
36 |
37 | func CreatePreviousCursor(position string) string {
38 | if position == "" {
39 | return ""
40 | }
41 |
42 | prevCursor := &Cursor{
43 | Direction: CursorDirectionPrevious,
44 | Position: position,
45 | }
46 | return prevCursor.String()
47 | }
48 |
--------------------------------------------------------------------------------
/core/taskengine/pagination_empty_test.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/AvaProtocol/EigenLayer-AVS/core/testutil"
7 | avsproto "github.com/AvaProtocol/EigenLayer-AVS/protobuf"
8 | "github.com/AvaProtocol/EigenLayer-AVS/storage"
9 | )
10 |
11 | func TestPaginationEmptyParameters(t *testing.T) {
12 | db := testutil.TestMustDB()
13 | defer storage.Destroy(db.(*storage.BadgerStorage))
14 |
15 | config := testutil.GetAggregatorConfig()
16 | n := New(db, config, nil, testutil.GetLogger())
17 |
18 | user := testutil.TestUser1()
19 |
20 | result, err := n.ListSecrets(user, &avsproto.ListSecretsReq{
21 | Before: "",
22 | After: "",
23 | Limit: 5,
24 | })
25 | if err != nil {
26 | t.Errorf("Expected no error when both before and after are empty (first page), got %v", err)
27 | }
28 | if result == nil {
29 | t.Errorf("Expected valid result for first page")
30 | }
31 |
32 | result, err = n.ListSecrets(user, &avsproto.ListSecretsReq{
33 | Before: "",
34 | After: "eyJkIjoibmV4dCIsInAiOiIwIn0=", // valid cursor
35 | Limit: 5,
36 | })
37 | if err != nil {
38 | t.Errorf("Expected no error for normal forward pagination, got %v", err)
39 | }
40 |
41 | result, err = n.ListSecrets(user, &avsproto.ListSecretsReq{
42 | Before: "eyJkIjoibmV4dCIsInAiOiIwIn0=", // valid cursor
43 | After: "",
44 | Limit: 5,
45 | })
46 | if err != nil {
47 | t.Errorf("Expected no error for normal backward pagination, got %v", err)
48 | }
49 |
50 | taskResult, err := n.ListTasksByUser(user, &avsproto.ListTasksReq{
51 | SmartWalletAddress: []string{"0x7c3a76086588230c7B3f4839A4c1F5BBafcd57C6"},
52 | Before: "",
53 | After: "",
54 | Limit: 5,
55 | })
56 | if err != nil {
57 | t.Errorf("Expected no error for first page of ListTasksByUser, got %v", err)
58 | }
59 | if taskResult == nil {
60 | t.Errorf("Expected valid result for first page of ListTasksByUser")
61 | }
62 |
63 | tr := testutil.JsFastTask()
64 | tr.SmartWalletAddress = "0x7c3a76086588230c7B3f4839A4c1F5BBafcd57C6"
65 | task, _ := n.CreateTask(user, tr)
66 |
67 | execResult, err := n.ListExecutions(user, &avsproto.ListExecutionsReq{
68 | TaskIds: []string{task.Id},
69 | Before: "",
70 | After: "",
71 | Limit: 5,
72 | })
73 | if err != nil {
74 | t.Errorf("Expected no error for first page of ListExecutions, got %v", err)
75 | }
76 | if execResult == nil {
77 | t.Errorf("Expected valid result for first page of ListExecutions")
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/core/taskengine/rest_response_processor.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 |
7 | "github.com/go-resty/resty/v2"
8 | )
9 |
10 | // ProcessRestAPIResponse processes a REST API response and returns the appropriate data format
11 | // For HTTP 2xx status codes: returns the body content directly
12 | // For non-2xx status codes: returns error format with {success: false, error: string, statusCode: int}
13 | func ProcessRestAPIResponse(response *resty.Response) map[string]interface{} {
14 | statusCode := response.StatusCode()
15 |
16 | // Parse response body
17 | var bodyData interface{}
18 | responseBody := response.Body()
19 | if len(responseBody) > 0 {
20 | // Try to parse as JSON first
21 | var jsonData interface{}
22 | if err := json.Unmarshal(responseBody, &jsonData); err == nil {
23 | bodyData = jsonData
24 | } else {
25 | // Fallback to string if not valid JSON
26 | bodyData = string(responseBody)
27 | }
28 | } else {
29 | bodyData = ""
30 | }
31 |
32 | // For HTTP 2xx status codes, return the body directly
33 | if statusCode >= 200 && statusCode < 300 {
34 | // If body is a map, return it directly; otherwise wrap it
35 | if bodyMap, ok := bodyData.(map[string]interface{}); ok {
36 | return bodyMap
37 | } else {
38 | return map[string]interface{}{"data": bodyData}
39 | }
40 | }
41 |
42 | // For non-2xx status codes, return error format
43 | return map[string]interface{}{
44 | "success": false,
45 | "error": fmt.Sprintf("HTTP %d error", statusCode),
46 | "statusCode": statusCode,
47 | "responseBody": bodyData,
48 | }
49 | }
50 |
51 | // ProcessRestAPIResponseRaw processes a raw REST API response map (from protobuf)
52 | // and returns the appropriate data format using the same logic as ProcessRestAPIResponse
53 | func ProcessRestAPIResponseRaw(responseData map[string]interface{}) map[string]interface{} {
54 | // Check if this has statusCode field
55 | statusCodeValue, hasStatus := responseData["statusCode"]
56 | if !hasStatus {
57 | // No status code, return as-is (shouldn't happen with proper REST responses)
58 | return responseData
59 | }
60 |
61 | var statusCode int
62 | switch sc := statusCodeValue.(type) {
63 | case int:
64 | statusCode = sc
65 | case float64:
66 | statusCode = int(sc)
67 | default:
68 | // Invalid status code type, return as-is
69 | return responseData
70 | }
71 |
72 | // Get body data
73 | bodyData, hasBody := responseData["body"]
74 | if !hasBody {
75 | bodyData = ""
76 | }
77 |
78 | // For HTTP 2xx status codes, return the body directly
79 | if statusCode >= 200 && statusCode < 300 {
80 | // If body is a map, return it directly; otherwise wrap it
81 | if bodyMap, ok := bodyData.(map[string]interface{}); ok {
82 | return bodyMap
83 | } else {
84 | return map[string]interface{}{"data": bodyData}
85 | }
86 | }
87 |
88 | // For non-2xx status codes, return error format
89 | return map[string]interface{}{
90 | "success": false,
91 | "error": fmt.Sprintf("HTTP %d error", statusCode),
92 | "statusCode": statusCode,
93 | "responseBody": bodyData,
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/core/taskengine/run_node_triggers_test.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/AvaProtocol/EigenLayer-AVS/storage"
7 | )
8 |
9 | func TestRunNodeImmediately_TriggerTypes(t *testing.T) {
10 | engine := createTestEngine()
11 | defer storage.Destroy(engine.db.(*storage.BadgerStorage))
12 |
13 | testCases := []struct {
14 | name string
15 | nodeConfig map[string]interface{}
16 | }{
17 | {
18 | name: "Block Trigger",
19 | nodeConfig: map[string]interface{}{
20 | "triggerType": "block",
21 | "interval": 1,
22 | },
23 | },
24 | {
25 | name: "Manual Trigger",
26 | nodeConfig: map[string]interface{}{
27 | "triggerType": "manual",
28 | },
29 | },
30 | {
31 | name: "Cron Trigger",
32 | nodeConfig: map[string]interface{}{
33 | "triggerType": "cron",
34 | "cronExpression": "0 0 * * *",
35 | },
36 | },
37 | {
38 | name: "Fixed Time Trigger",
39 | nodeConfig: map[string]interface{}{
40 | "triggerType": "fixedTime",
41 | "timestamp": 1672531200,
42 | },
43 | },
44 | {
45 | name: "Event Trigger",
46 | nodeConfig: map[string]interface{}{
47 | "triggerType": "event",
48 | "contractAddress": "0x1234567890123456789012345678901234567890",
49 | "eventSignature": "Transfer(address,address,uint256)",
50 | },
51 | },
52 | }
53 |
54 | for _, tc := range testCases {
55 | t.Run(tc.name, func(t *testing.T) {
56 | result, err := engine.RunNodeImmediately("trigger", tc.nodeConfig, map[string]interface{}{})
57 |
58 | if err != nil {
59 | t.Errorf("Expected no error for %s, got: %v", tc.name, err)
60 | }
61 |
62 | if result == nil {
63 | t.Errorf("Expected result for %s, got nil", tc.name)
64 | }
65 | })
66 | }
67 | }
68 |
69 | func TestRunTriggerRPC_ManualTrigger(t *testing.T) {
70 | engine := createTestEngine()
71 | defer storage.Destroy(engine.db.(*storage.BadgerStorage))
72 |
73 | nodeConfig := map[string]interface{}{
74 | "triggerType": "manual",
75 | "runAt": 1672531200,
76 | }
77 |
78 | result, err := engine.RunNodeImmediately("manualTrigger", nodeConfig, map[string]interface{}{})
79 |
80 | if err != nil {
81 | t.Errorf("Expected no error, got: %v", err)
82 | }
83 |
84 | if result == nil {
85 | t.Errorf("Expected result, got nil")
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/core/taskengine/secret.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/AvaProtocol/EigenLayer-AVS/model"
7 | "github.com/AvaProtocol/EigenLayer-AVS/storage"
8 | "github.com/ethereum/go-ethereum/common"
9 | )
10 |
11 | func LoadSecretForTask(db storage.Storage, task *model.Task) (map[string]string, error) {
12 | secrets := map[string]string{}
13 |
14 | if task.Owner == "" {
15 | return nil, fmt.Errorf("missing user in task structure")
16 | }
17 |
18 | user := &model.User{
19 | Address: common.HexToAddress(task.Owner),
20 | }
21 |
22 | prefixes := []string{
23 | SecretStoragePrefix(user),
24 | }
25 |
26 | secretKeys, err := db.ListKeysMulti(prefixes)
27 | if err != nil {
28 | return nil, err
29 | }
30 | // Copy global static secret we loaded from config file.
31 | copyMap(secrets, macroSecrets)
32 |
33 | // Load secret at user level. It has higher priority
34 | // TODO: load secret at org level first, when we introduce that
35 | for _, k := range secretKeys {
36 | secretWithNameOnly := SecretNameFromKey(k)
37 | if secretWithNameOnly.WorkflowID == "" {
38 | if value, err := db.GetKey([]byte(k)); err == nil {
39 | secrets[secretWithNameOnly.Name] = string(value)
40 | }
41 | }
42 | }
43 |
44 | // Now we get secret at workflow level, the lowest level.
45 | for _, k := range secretKeys {
46 | secretWithNameOnly := SecretNameFromKey(k)
47 | if _, ok := secrets[secretWithNameOnly.Name]; ok {
48 | // Our priority is define in this issue: https://github.com/AvaProtocol/EigenLayer-AVS/issues/104#issue-2793661337
49 | // Regarding the scope of permissions, the top level permission could always overwrite lower levels. For example, org > user > workflow
50 | continue
51 | }
52 |
53 | if secretWithNameOnly.WorkflowID == task.Id {
54 | if value, err := db.GetKey([]byte(k)); err == nil {
55 | secrets[secretWithNameOnly.Name] = string(value)
56 | }
57 | }
58 | }
59 |
60 | return secrets, nil
61 | }
62 |
63 | // copyMap is a replacement for maps.Copy for Go 1.18.1 compatibility
64 | func copyMap(dst, src map[string]string) {
65 | for k, v := range src {
66 | dst[k] = v
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/core/taskengine/stats.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/AvaProtocol/EigenLayer-AVS/model"
7 | avsproto "github.com/AvaProtocol/EigenLayer-AVS/protobuf"
8 | "github.com/AvaProtocol/EigenLayer-AVS/storage"
9 | )
10 |
11 | type StatService struct {
12 | db storage.Storage
13 | }
14 |
15 | func NewStatService(db storage.Storage) *StatService {
16 | return &StatService{
17 | db: db,
18 | }
19 | }
20 |
21 | func (svc *StatService) GetTaskCount(smartWalletAddress *model.SmartWallet) (*model.SmartWalletTaskStat, error) {
22 | stat := &model.SmartWalletTaskStat{}
23 |
24 | prefix := SmartWalletTaskStoragePrefix(*smartWalletAddress.Owner, *smartWalletAddress.Address)
25 | items, err := svc.db.GetByPrefix(prefix)
26 | if err != nil {
27 | return stat, err
28 | }
29 |
30 | for _, item := range items {
31 | taskStatus, _ := strconv.ParseInt(string(item.Value), 10, 32)
32 | stat.Total += 1
33 | switch avsproto.TaskStatus(taskStatus) {
34 | case avsproto.TaskStatus_Active:
35 | stat.Active += 1
36 | case avsproto.TaskStatus_Completed:
37 | stat.Completed += 1
38 | case avsproto.TaskStatus_Failed:
39 | stat.Failed += 1
40 | case avsproto.TaskStatus_Canceled:
41 | stat.Canceled += 1
42 | }
43 | }
44 |
45 | return stat, nil
46 | }
47 |
--------------------------------------------------------------------------------
/core/taskengine/stats_test.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "testing"
7 |
8 | "github.com/AvaProtocol/EigenLayer-AVS/core/testutil"
9 | "github.com/AvaProtocol/EigenLayer-AVS/model"
10 | avsproto "github.com/AvaProtocol/EigenLayer-AVS/protobuf"
11 | "github.com/AvaProtocol/EigenLayer-AVS/storage"
12 | )
13 |
14 | func TestTaskStatCount(t *testing.T) {
15 | db := testutil.TestMustDB()
16 | defer storage.Destroy(db.(*storage.BadgerStorage))
17 |
18 | config := testutil.GetAggregatorConfig()
19 | n := New(db, config, nil, testutil.GetLogger())
20 |
21 | user1 := testutil.TestUser1()
22 |
23 | // Populate task
24 | tr1 := testutil.RestTask()
25 | tr1.Name = "t1"
26 | tr1.MaxExecution = 1
27 | // salt 0
28 | tr1.SmartWalletAddress = "0x7c3a76086588230c7B3f4839A4c1F5BBafcd57C6"
29 | n.CreateTask(testutil.TestUser1(), tr1)
30 |
31 | statSvc := NewStatService(db)
32 | result, _ := statSvc.GetTaskCount(user1.ToSmartWallet())
33 |
34 | if !reflect.DeepEqual(
35 | result, &model.SmartWalletTaskStat{
36 | Total: 1,
37 | Active: 1,
38 | }) {
39 | t.Errorf("expect task total is 1, but got %v", result)
40 | }
41 | }
42 |
43 | func TestTaskStatCountCompleted(t *testing.T) {
44 | db := testutil.TestMustDB()
45 | defer storage.Destroy(db.(*storage.BadgerStorage))
46 |
47 | user1 := testutil.TestUser1()
48 |
49 | task1 := &model.Task{
50 | Task: &avsproto.Task{
51 | Owner: user1.Address.Hex(),
52 | SmartWalletAddress: user1.SmartAccountAddress.Hex(),
53 | Id: "t1",
54 | },
55 | }
56 |
57 | db.Set(TaskUserKey(task1), []byte(fmt.Sprintf("%d", avsproto.TaskStatus_Completed)))
58 |
59 | statSvc := NewStatService(db)
60 | result, _ := statSvc.GetTaskCount(user1.ToSmartWallet())
61 |
62 | if !reflect.DeepEqual(
63 | result, &model.SmartWalletTaskStat{
64 | Total: 1,
65 | Active: 0,
66 | Completed: 1,
67 | }) {
68 | t.Errorf("expect task total is 1, completed is 1 but got %v", result)
69 | }
70 | }
71 |
72 | func TestTaskStatCountAllStatus(t *testing.T) {
73 | db := testutil.TestMustDB()
74 | defer storage.Destroy(db.(*storage.BadgerStorage))
75 |
76 | user1 := testutil.TestUser1()
77 |
78 | task1 := &model.Task{
79 | Task: &avsproto.Task{
80 | Owner: user1.Address.Hex(),
81 | SmartWalletAddress: user1.SmartAccountAddress.Hex(),
82 | Id: "t1",
83 | },
84 | }
85 |
86 | task2 := &model.Task{
87 | Task: &avsproto.Task{
88 | Owner: user1.Address.Hex(),
89 | SmartWalletAddress: user1.SmartAccountAddress.Hex(),
90 | Id: "t2",
91 | },
92 | }
93 |
94 | task3 := &model.Task{
95 | Task: &avsproto.Task{
96 | Owner: user1.Address.Hex(),
97 | SmartWalletAddress: user1.SmartAccountAddress.Hex(),
98 | Id: "t3",
99 | },
100 | }
101 |
102 | task4 := &model.Task{
103 | Task: &avsproto.Task{
104 | Owner: user1.Address.Hex(),
105 | SmartWalletAddress: user1.SmartAccountAddress.Hex(),
106 | Id: "t4",
107 | },
108 | }
109 |
110 | db.Set(TaskUserKey(task1), []byte(fmt.Sprintf("%d", avsproto.TaskStatus_Completed)))
111 | db.Set(TaskUserKey(task2), []byte(fmt.Sprintf("%d", avsproto.TaskStatus_Failed)))
112 | db.Set(TaskUserKey(task3), []byte(fmt.Sprintf("%d", avsproto.TaskStatus_Canceled)))
113 | db.Set(TaskUserKey(task4), []byte(fmt.Sprintf("%d", avsproto.TaskStatus_Active)))
114 |
115 | statSvc := NewStatService(db)
116 | result, _ := statSvc.GetTaskCount(user1.ToSmartWallet())
117 |
118 | if !reflect.DeepEqual(
119 | result, &model.SmartWalletTaskStat{
120 | Total: 4,
121 | Active: 1,
122 | Completed: 1,
123 | Failed: 1,
124 | Canceled: 1,
125 | }) {
126 | t.Errorf("expect task total=4, active=1, completed=1, failed=1, canceled=1, but got %v", result)
127 | }
128 |
129 | }
130 |
--------------------------------------------------------------------------------
/core/taskengine/token_metadata_service_test.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "os"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | // TestTokenEnrichmentService_WhitelistOnly tests the TokenEnrichmentService
11 | // in whitelist-only mode (without RPC client)
12 | func TestTokenEnrichmentService_WhitelistOnly(t *testing.T) {
13 | logger := &MockLogger{}
14 |
15 | // Change to project root directory so we can find token_whitelist/
16 | originalWd, _ := os.Getwd()
17 | defer os.Chdir(originalWd)
18 |
19 | // Go up two levels: from core/taskengine to project root
20 | err := os.Chdir("../..")
21 | require.NoError(t, err)
22 |
23 | // Create service without RPC client (whitelist-only mode)
24 | service, err := NewTokenEnrichmentService(nil, logger)
25 | require.NoError(t, err)
26 |
27 | // Load whitelist
28 | err = service.LoadWhitelist()
29 | require.NoError(t, err)
30 |
31 | // Test that cache has data
32 | require.Greater(t, service.GetCacheSize(), 0, "Cache should contain whitelist data")
33 |
34 | t.Run("should find tokens in whitelist", func(t *testing.T) {
35 | // Test with USDC address from whitelist (uses the detected chain)
36 | // Note: This test works with whatever whitelist is loaded based on chain ID
37 | metadata, err := service.GetTokenMetadata("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
38 | require.NoError(t, err)
39 |
40 | if metadata != nil {
41 | // If found in whitelist, verify it has proper data
42 | require.Equal(t, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", metadata.Address)
43 | require.NotEmpty(t, metadata.Name)
44 | require.NotEmpty(t, metadata.Symbol)
45 | require.Equal(t, "whitelist", metadata.Source)
46 | } else {
47 | // If not found, it means this address isn't in the current chain's whitelist
48 | t.Logf("USDC address not found in whitelist for chain %d", service.GetChainID())
49 | }
50 | })
51 |
52 | t.Run("should not find non-existent tokens", func(t *testing.T) {
53 | fakeAddress := "0x1234567890123456789012345678901234567890"
54 | metadata, err := service.GetTokenMetadata(fakeAddress)
55 | require.NoError(t, err)
56 | require.Nil(t, metadata, "Fake address should not be found in whitelist-only mode")
57 | })
58 |
59 | t.Run("should handle address normalization", func(t *testing.T) {
60 | // Test with first token in cache (to ensure it exists)
61 | if service.GetCacheSize() > 0 {
62 | // Get an address that we know exists
63 | testAddr := "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
64 | lowercaseAddr := testAddr
65 | uppercaseAddr := "0xA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48"
66 |
67 | // Get metadata for lowercase
68 | metadataLower, err := service.GetTokenMetadata(lowercaseAddr)
69 | require.NoError(t, err)
70 |
71 | // Get metadata for uppercase
72 | metadataUpper, err := service.GetTokenMetadata(uppercaseAddr)
73 | require.NoError(t, err)
74 |
75 | // Both should return the same result
76 | if metadataLower != nil && metadataUpper != nil {
77 | require.Equal(t, metadataLower.Address, metadataUpper.Address, "Both should return lowercase address")
78 | require.Equal(t, metadataLower.Name, metadataUpper.Name)
79 | require.Equal(t, metadataLower.Symbol, metadataUpper.Symbol)
80 | }
81 | }
82 | })
83 |
84 | t.Run("should report correct cache size", func(t *testing.T) {
85 | cacheSize := service.GetCacheSize()
86 | require.Greater(t, cacheSize, 0, "Cache should contain tokens after loading whitelist")
87 |
88 | // Cache size should be reasonable (not negative, not absurdly large)
89 | require.LessOrEqual(t, cacheSize, 1000, "Cache size should be reasonable")
90 | })
91 | }
92 |
--------------------------------------------------------------------------------
/core/taskengine/trigger/common.go:
--------------------------------------------------------------------------------
1 | package trigger
2 |
3 | import (
4 | "math/big"
5 | "sync"
6 | "time"
7 |
8 | sdklogging "github.com/Layr-Labs/eigensdk-go/logging"
9 | "github.com/ethereum/go-ethereum/ethclient"
10 | )
11 |
12 | var (
13 | zero = big.NewInt(0)
14 | )
15 |
16 | type RpcOption struct {
17 | RpcURL string
18 | WsRpcURL string
19 | }
20 |
21 | type CommonTrigger struct {
22 | wsEthClient *ethclient.Client
23 | ethClient *ethclient.Client
24 | rpcOption *RpcOption
25 |
26 | logger sdklogging.Logger
27 |
28 | // channel to track shutdown
29 | done chan bool
30 | shutdown bool
31 | mu sync.Mutex
32 |
33 | // a counter to track progress of the trigger. the counter will increase everytime a processing happen
34 | progress int64
35 | }
36 |
37 | func (b *CommonTrigger) retryConnectToRpc() error {
38 | for {
39 | if b.shutdown {
40 | return nil
41 | }
42 |
43 | conn, err := ethclient.Dial(b.rpcOption.WsRpcURL)
44 | if err == nil {
45 | b.wsEthClient = conn
46 | return nil
47 | }
48 | b.logger.Errorf("cannot establish websocket client for RPC, retry in 15 seconds", "err", err)
49 | time.Sleep(15 * time.Second)
50 | }
51 | }
52 |
53 | func (b *CommonTrigger) Shutdown() {
54 | b.shutdown = true
55 | b.done <- true
56 | }
57 |
58 | func (b *CommonTrigger) GetProgress() int64 {
59 | return b.progress
60 | }
61 |
--------------------------------------------------------------------------------
/core/taskengine/trigger_helper.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | avsproto "github.com/AvaProtocol/EigenLayer-AVS/protobuf"
5 | sdklogging "github.com/Layr-Labs/eigensdk-go/logging"
6 | )
7 |
8 | // TriggerData represents the flattened trigger information
9 | type TriggerData struct {
10 | Type avsproto.TriggerType
11 | Output interface{} // Will hold the specific trigger output (BlockTrigger.Output, etc.)
12 | }
13 |
14 | // ExtractTriggerOutput extracts trigger output from any oneof field (TriggerTaskReq or NotifyTriggersReq)
15 | func ExtractTriggerOutput(triggerOutput interface{}) interface{} {
16 | switch output := triggerOutput.(type) {
17 | // TriggerTaskReq oneof cases
18 | case *avsproto.TriggerTaskReq_BlockTrigger:
19 | return output.BlockTrigger
20 | case *avsproto.TriggerTaskReq_FixedTimeTrigger:
21 | return output.FixedTimeTrigger
22 | case *avsproto.TriggerTaskReq_CronTrigger:
23 | return output.CronTrigger
24 | case *avsproto.TriggerTaskReq_EventTrigger:
25 | return output.EventTrigger
26 | case *avsproto.TriggerTaskReq_ManualTrigger:
27 | return output.ManualTrigger
28 |
29 | // NotifyTriggersReq oneof cases
30 | case *avsproto.NotifyTriggersReq_BlockTrigger:
31 | return output.BlockTrigger
32 | case *avsproto.NotifyTriggersReq_FixedTimeTrigger:
33 | return output.FixedTimeTrigger
34 | case *avsproto.NotifyTriggersReq_CronTrigger:
35 | return output.CronTrigger
36 | case *avsproto.NotifyTriggersReq_EventTrigger:
37 | return output.EventTrigger
38 | case *avsproto.NotifyTriggersReq_ManualTrigger:
39 | return output.ManualTrigger
40 | }
41 | return nil
42 | }
43 |
44 | func GetTriggerReasonOrDefault(queueData *QueueExecutionData, taskID string, logger sdklogging.Logger) *TriggerData {
45 | if queueData != nil && queueData.TriggerType != avsproto.TriggerType_TRIGGER_TYPE_UNSPECIFIED {
46 | return &TriggerData{
47 | Type: queueData.TriggerType,
48 | Output: queueData.TriggerOutput,
49 | }
50 | }
51 |
52 | logger.Debug("Creating default trigger data", "task_id", taskID)
53 | return &TriggerData{
54 | Type: avsproto.TriggerType_TRIGGER_TYPE_UNSPECIFIED,
55 | Output: nil,
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/core/taskengine/validation.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "github.com/AvaProtocol/EigenLayer-AVS/model"
5 | "github.com/AvaProtocol/EigenLayer-AVS/storage"
6 | "github.com/ethereum/go-ethereum/common"
7 | )
8 |
9 | const TaskIDLength = 26
10 |
11 | func ValidWalletAddress(address string) bool {
12 | return common.IsHexAddress(address)
13 | }
14 |
15 | func ValidWalletOwner(db storage.Storage, u *model.User, smartWalletAddress common.Address) (bool, error) {
16 | // the smart wallet address is the default one
17 | if u.SmartAccountAddress.Hex() == smartWalletAddress.Hex() {
18 | return true, nil
19 | }
20 |
21 | // not default, look up in our storage
22 | exists, err := db.Exist([]byte(WalletStorageKey(u.Address, smartWalletAddress.Hex())))
23 | if exists {
24 | return true, nil
25 | }
26 |
27 | return false, err
28 | }
29 |
30 | func ValidateTaskId(id string) bool {
31 | if len(id) != TaskIDLength {
32 | return false
33 | }
34 |
35 | return true
36 | }
37 |
--------------------------------------------------------------------------------
/core/taskengine/validation_test.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/ethereum/go-ethereum/common"
7 |
8 | "github.com/AvaProtocol/EigenLayer-AVS/model"
9 | "github.com/AvaProtocol/EigenLayer-AVS/storage"
10 |
11 | "github.com/AvaProtocol/EigenLayer-AVS/core/testutil"
12 | )
13 |
14 | func TestWalletOwnerReturnTrueForDefaultAddress(t *testing.T) {
15 | smartAddress := common.HexToAddress("0x5Df343de7d99fd64b2479189692C1dAb8f46184a")
16 |
17 | result, err := ValidWalletOwner(nil, &model.User{
18 | Address: common.HexToAddress("0xe272b72E51a5bF8cB720fc6D6DF164a4D5E321C5"),
19 | SmartAccountAddress: &smartAddress,
20 | }, common.HexToAddress("0x5Df343de7d99fd64b2479189692C1dAb8f46184a"))
21 |
22 | if !result || err != nil {
23 | t.Errorf("expect true, got false")
24 | }
25 | }
26 |
27 | func TestWalletOwnerReturnTrueForNonDefaultAddress(t *testing.T) {
28 | db := testutil.TestMustDB()
29 | defer storage.Destroy(db.(*storage.BadgerStorage))
30 |
31 | eoa := common.HexToAddress("0xe272b72E51a5bF8cB720fc6D6DF164a4D5E321C5")
32 | defaultSmartWallet := common.HexToAddress("0x5Df343de7d99fd64b2479189692C1dAb8f46184a")
33 | customSmartWallet := common.HexToAddress("0xdD85693fd14b522a819CC669D6bA388B4FCd158d")
34 |
35 | result, err := ValidWalletOwner(db, &model.User{
36 | Address: eoa,
37 | SmartAccountAddress: &defaultSmartWallet,
38 | }, customSmartWallet)
39 | if result == true {
40 | t.Errorf("expect 0xdD85693fd14b522a819CC669D6bA388B4FCd158d not owned by 0xe272b72E51a5bF8cB720fc6D6DF164a4D5E321C5, got true")
41 | }
42 |
43 | // setup wallet binding
44 | db.Set([]byte(WalletStorageKey(eoa, customSmartWallet.Hex())), []byte("1"))
45 |
46 | result, err = ValidWalletOwner(db, &model.User{
47 | Address: eoa,
48 | SmartAccountAddress: &defaultSmartWallet,
49 | }, customSmartWallet)
50 | if !result || err != nil {
51 | t.Errorf("expect 0xdD85693fd14b522a819CC669D6bA388B4FCd158d owned by 0xe272b72E51a5bF8cB720fc6D6DF164a4D5E321C5, got false")
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/core/taskengine/vm_runner_graphql_query.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/url"
7 | "strings"
8 | "time"
9 |
10 | "github.com/AvaProtocol/EigenLayer-AVS/pkg/graphql"
11 | avsproto "github.com/AvaProtocol/EigenLayer-AVS/protobuf"
12 | "google.golang.org/protobuf/types/known/anypb"
13 | "google.golang.org/protobuf/types/known/structpb"
14 | )
15 |
16 | type GraphqlQueryProcessor struct {
17 | *CommonProcessor
18 |
19 | client *graphql.Client
20 | sb *strings.Builder
21 | url *url.URL
22 | }
23 |
24 | func NewGraphqlQueryProcessor(vm *VM) (*GraphqlQueryProcessor, error) {
25 | sb := &strings.Builder{}
26 |
27 | return &GraphqlQueryProcessor{
28 | client: nil, // Will be initialized when we have the URL from Config
29 | sb: sb,
30 | url: nil, // Will be set when we have the URL from Config
31 |
32 | CommonProcessor: &CommonProcessor{vm},
33 | }, nil
34 | }
35 |
36 | func (r *GraphqlQueryProcessor) Execute(stepID string, node *avsproto.GraphQLQueryNode) (*avsproto.Execution_Step, any, error) {
37 | ctx := context.Background()
38 | t0 := time.Now().UnixMilli()
39 |
40 | // Look up the task node to get the name
41 | var nodeName string = "unknown"
42 | r.vm.mu.Lock()
43 | if taskNode, exists := r.vm.TaskNodes[stepID]; exists {
44 | nodeName = taskNode.Name
45 | }
46 | r.vm.mu.Unlock()
47 |
48 | step := &avsproto.Execution_Step{
49 | Id: stepID,
50 | Log: "",
51 | OutputData: nil,
52 | Success: true,
53 | Error: "",
54 | StartAt: t0,
55 | Type: avsproto.NodeType_NODE_TYPE_GRAPHQL_QUERY.String(),
56 | Name: nodeName,
57 | }
58 |
59 | var err error
60 | defer func() {
61 | step.EndAt = time.Now().UnixMilli()
62 | step.Success = err == nil
63 | if err != nil {
64 | step.Error = err.Error()
65 | }
66 | }()
67 |
68 | // Get configuration from Config message (static configuration)
69 | if node.Config == nil {
70 | err = fmt.Errorf("GraphQLQueryNode Config is nil")
71 | return step, nil, err
72 | }
73 |
74 | endpoint := node.Config.Url
75 | queryStr := node.Config.Query
76 |
77 | if endpoint == "" || queryStr == "" {
78 | err = fmt.Errorf("missing required configuration: url and query")
79 | return step, nil, err
80 | }
81 |
82 | // Preprocess URL and query for template variables
83 | endpoint = r.vm.preprocessTextWithVariableMapping(endpoint)
84 | queryStr = r.vm.preprocessTextWithVariableMapping(queryStr)
85 |
86 | // Initialize client with the URL from Config
87 | log := func(s string) {
88 | r.sb.WriteString(s)
89 | }
90 |
91 | client, err := graphql.NewClient(endpoint, log)
92 | if err != nil {
93 | return step, nil, err
94 | }
95 |
96 | u, err := url.Parse(endpoint)
97 | if err != nil {
98 | return step, nil, err
99 | }
100 |
101 | var resp map[string]any
102 | r.sb.WriteString(fmt.Sprintf("Execute GraphQL %s at %s", u.Hostname(), time.Now()))
103 | query := graphql.NewRequest(queryStr)
104 | err = client.Run(ctx, query, &resp)
105 | if err != nil {
106 | return step, nil, err
107 | }
108 |
109 | step.Log = r.sb.String()
110 |
111 | value, err := structpb.NewValue(resp)
112 | if err == nil {
113 | pbResult, _ := anypb.New(value)
114 | step.OutputData = &avsproto.Execution_Step_Graphql{
115 | Graphql: &avsproto.GraphQLQueryNode_Output{
116 | Data: pbResult,
117 | },
118 | }
119 |
120 | }
121 |
122 | r.SetOutputVarForStep(stepID, resp)
123 | return step, resp, err
124 | }
125 |
--------------------------------------------------------------------------------
/core/taskengine/vm_runner_graphql_query_test.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "strings"
5 | "testing"
6 |
7 | "github.com/AvaProtocol/EigenLayer-AVS/core/testutil"
8 | "github.com/AvaProtocol/EigenLayer-AVS/model"
9 | "github.com/AvaProtocol/EigenLayer-AVS/pkg/gow"
10 | avsproto "github.com/AvaProtocol/EigenLayer-AVS/protobuf"
11 | )
12 |
13 | // Test make a query to a demo graphql server to ensure our node processing work
14 | func TestGraphlQlNodeSimpleQuery(t *testing.T) {
15 | node := &avsproto.GraphQLQueryNode{
16 | Config: &avsproto.GraphQLQueryNode_Config{
17 | Url: "https://spacex-production.up.railway.app/",
18 | Query: `
19 | query Launch {
20 | company {
21 | ceo
22 | }
23 | launches(limit: 2, sort: "launch_date_unix", order: "ASC") {
24 | id
25 | mission_name
26 | }
27 | }
28 | `,
29 | },
30 | }
31 |
32 | nodes := []*avsproto.TaskNode{
33 | {
34 | Id: "123abc",
35 | Name: "graphqlQuery",
36 | TaskType: &avsproto.TaskNode_GraphqlQuery{
37 | GraphqlQuery: node,
38 | },
39 | },
40 | }
41 |
42 | trigger := &avsproto.TaskTrigger{
43 | Id: "triggertest",
44 | Name: "triggertest",
45 | }
46 |
47 | edges := []*avsproto.TaskEdge{
48 | {
49 | Id: "e1",
50 | Source: trigger.Id,
51 | Target: "123abc",
52 | },
53 | }
54 |
55 | vm, err := NewVMWithData(&model.Task{
56 | Task: &avsproto.Task{
57 | Id: "123abc",
58 | Nodes: nodes,
59 | Edges: edges,
60 | Trigger: trigger,
61 | },
62 | }, nil, testutil.GetTestSmartWalletConfig(), nil)
63 | if err != nil {
64 | t.Errorf("failed to create VM: %v", err)
65 | return
66 | }
67 |
68 | n, _ := NewGraphqlQueryProcessor(vm)
69 |
70 | step, _, err := n.Execute("123abc", node)
71 |
72 | if err != nil {
73 | t.Errorf("expected rest node run succesfull but got error: %v", err)
74 | }
75 |
76 | if !step.Success {
77 | t.Errorf("expected rest node run successfully but failed")
78 | }
79 |
80 | if !strings.Contains(step.Log, "Execute GraphQL spacex-production.up.railway.app") {
81 | t.Errorf("expected log contains request trace data but not found. Log data is: %s", step.Log)
82 | }
83 |
84 | if step.Error != "" {
85 | t.Errorf("expected log contains request trace data but found no")
86 | }
87 |
88 | var output struct {
89 | Company struct {
90 | CEO string `json:"ceo"`
91 | } `json:"company"`
92 | Launches []struct {
93 | ID string `json:"id"`
94 | MissionName string `json:"mission_name"`
95 | } `json:"launches"`
96 | }
97 |
98 | err = gow.AnyToStruct(step.GetGraphql().Data, &output)
99 | if err != nil {
100 | t.Errorf("expected the data output in json format, but failed to decode %v", err)
101 | }
102 |
103 | if len(output.Launches) != 2 {
104 | t.Errorf("expected 2 launches but found %d", len(output.Launches))
105 | }
106 | if output.Launches[0].ID != "5eb87cd9ffd86e000604b32a" {
107 | t.Errorf("id doesn't match. expected %s got %s", "5eb87cd9ffd86e000604b32a", output.Launches[0].ID)
108 | }
109 |
110 | if output.Launches[0].MissionName != "FalconSat" {
111 | t.Errorf("name doesn't match. expected %s got %s", "FalconSat", output.Launches[0].MissionName)
112 | }
113 |
114 | if output.Launches[1].ID != "5eb87cdaffd86e000604b32b" {
115 | t.Errorf("id doesn't match. expected %s got %s", "5eb87cd9ffd86e000604b32b", output.Launches[0].ID)
116 | }
117 |
118 | if output.Launches[1].MissionName != "DemoSat" {
119 | t.Errorf("name doesn't match. expected %s got %s", "DemoSat", output.Launches[0].MissionName)
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/core/taskengine/whitelist.go:
--------------------------------------------------------------------------------
1 | package taskengine
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/ethereum/go-ethereum/common"
7 | )
8 |
9 | func isWhitelistedAddress(address common.Address, whitelist []common.Address) bool {
10 | if whitelist == nil {
11 | return false
12 | }
13 |
14 | addressStr := strings.ToLower(address.Hex())
15 | for _, whitelistAddr := range whitelist {
16 | if strings.ToLower(whitelistAddr.Hex()) == addressStr {
17 | return true
18 | }
19 | }
20 | return false
21 | }
22 |
--------------------------------------------------------------------------------
/core/utils.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "math/big"
5 |
6 | cstaskmanager "github.com/AvaProtocol/EigenLayer-AVS/contracts/bindings/AutomationTaskManager"
7 | "github.com/Layr-Labs/eigensdk-go/crypto/bls"
8 | "github.com/ethereum/go-ethereum/accounts/abi"
9 | "golang.org/x/crypto/sha3"
10 | )
11 |
12 | // this hardcodes abi.encode() for cstaskmanager.IAutomationTaskManagerTaskResponse
13 | // unclear why abigen doesn't provide this out of the box...
14 | func AbiEncodeTaskResponse(h *cstaskmanager.IAutomationTaskManagerTaskResponse) ([]byte, error) {
15 | // The order here has to match the field ordering of cstaskmanager.IAutomationTaskManagerTaskResponse
16 | taskResponseType, err := abi.NewType("tuple", "", []abi.ArgumentMarshaling{
17 | {
18 | Name: "referenceTaskIndex",
19 | Type: "uint32",
20 | },
21 | {
22 | Name: "numberSquared",
23 | Type: "uint256",
24 | },
25 | })
26 | if err != nil {
27 | return nil, err
28 | }
29 | arguments := abi.Arguments{
30 | abi.Argument{
31 | Type: taskResponseType,
32 | },
33 | }
34 |
35 | bytes, err := arguments.Pack(h)
36 | if err != nil {
37 | return nil, err
38 | }
39 |
40 | return bytes, nil
41 | }
42 |
43 | // GetTaskResponseDigest returns the hash of the TaskResponse, which is what operators sign over
44 | func GetTaskResponseDigest(h *cstaskmanager.IAutomationTaskManagerTaskResponse) ([32]byte, error) {
45 | encodeTaskResponseByte, err := AbiEncodeTaskResponse(h)
46 | if err != nil {
47 | return [32]byte{}, err
48 | }
49 |
50 | var taskResponseDigest [32]byte
51 | hasher := sha3.NewLegacyKeccak256()
52 | hasher.Write(encodeTaskResponseByte)
53 | copy(taskResponseDigest[:], hasher.Sum(nil)[:32])
54 |
55 | return taskResponseDigest, nil
56 | }
57 |
58 | // BINDING UTILS - conversion from contract structs to golang structs
59 |
60 | // BN254.sol is a library, so bindings for G1 Points and G2 Points are only generated
61 | // in every contract that imports that library. Thus the output here will need to be
62 | // type casted if G1Point is needed to interface with another contract (eg: BLSPublicKeyCompendium.sol)
63 | func ConvertToBN254G1Point(input *bls.G1Point) cstaskmanager.BN254G1Point {
64 | output := cstaskmanager.BN254G1Point{
65 | X: input.X.BigInt(big.NewInt(0)),
66 | Y: input.Y.BigInt(big.NewInt(0)),
67 | }
68 | return output
69 | }
70 |
71 | func ConvertToBN254G2Point(input *bls.G2Point) cstaskmanager.BN254G2Point {
72 | output := cstaskmanager.BN254G2Point{
73 | X: [2]*big.Int{input.X.A1.BigInt(big.NewInt(0)), input.X.A0.BigInt(big.NewInt(0))},
74 | Y: [2]*big.Int{input.Y.A1.BigInt(big.NewInt(0)), input.Y.A0.BigInt(big.NewInt(0))},
75 | }
76 | return output
77 | }
78 |
--------------------------------------------------------------------------------
/docker-compose-test.yml:
--------------------------------------------------------------------------------
1 | name: bundler-unitest
2 | services:
3 | bundler:
4 | image: ghcr.io/candidelabs/voltaire/voltaire-bundler:latest
5 | command:
6 | - "--bundler_secret"
7 | # For unit test, we use private key of wallet 0xE164dd09e720640F6695cB6cED0308065ceFECd9
8 | - "$BUNDLER_PRIVATE_KEY"
9 | - "--rpc_url"
10 | - "0.0.0.0"
11 | - "--rpc_port"
12 | - "4437"
13 | - "--ethereum_node_url"
14 | - "$ETH_RPC_URL"
15 | - "--chain_id"
16 | - "84532"
17 | - "--verbose"
18 | - "--unsafe"
19 | - "--disable_p2p"
20 | environment:
21 | - BUNDLER_PRIVATE_KEY
22 | # For unit test, we run against base sepolia because it's way more faster and cost less than even sepolia so we don't need to use a lot of test token.
23 | - ETH_RPC_URL="${BASE_SEPOLIA_RPC_URL}"
24 | ports:
25 | - "3437:4437"
26 | restart: always
27 | networks:
28 | - bundler-unitest
29 |
30 | networks:
31 | # The presence of these objects is sufficient to define them
32 | bundler-unitest: {}
33 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | name: ava
2 | services:
3 | aggregator:
4 | image: avaprotocol/avs
5 | command: ["aggregator"]
6 | build:
7 | context: .
8 | dockerfile: dockerfiles/aggregator.Dockerfile
9 | ports:
10 | - "2206:2206"
11 | - "1323:1323"
12 | networks:
13 | - ava
14 | #
15 | # backend:
16 | # image: example/database
17 | # volumes:
18 | # - db-data:/etc/data
19 | # networks:
20 | # - back-tier
21 | volumes:
22 | - ./config:/app/config
23 | - db-data:/tmp/app-avs
24 | healthcheck:
25 | test: curl --fail http://localhost:1323/up || exit 1
26 | interval: 3s
27 | retries: 15
28 | start_period: 3s
29 | timeout: 3s
30 | grpcui:
31 | image: fullstorydev/grpcui
32 | command: ["-plaintext", "aggregator:2206"]
33 | ports:
34 | - "8080:8080"
35 | networks:
36 | - ava
37 | depends_on:
38 | aggregator:
39 | condition: service_healthy
40 | volumes:
41 | db-data:
42 |
43 | networks:
44 | # The presence of these objects is sufficient to define them
45 | ava: {}
46 |
--------------------------------------------------------------------------------
/dockerfiles/aggregator.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.22.1-alpine AS builder
2 | ARG TARGETARCH
3 |
4 | WORKDIR /app
5 |
6 | COPY go.mod go.sum ./
7 |
8 | RUN go mod download
9 |
10 | COPY . ./
11 |
12 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -v -o /ava
13 |
14 |
15 | FROM debian:stable-slim
16 |
17 | WORKDIR /app
18 |
19 | RUN useradd -ms /bin/bash ava && \
20 | apt update && apt-get install -y ca-certificates socat telnet
21 |
22 | COPY --from=builder /ava /ava
23 |
24 | ENTRYPOINT ["/ava"]
25 |
--------------------------------------------------------------------------------
/dockerfiles/operator.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.22.1-alpine AS builder
2 | ARG RELEASE_TAG
3 | ARG COMMIT_SHA
4 | ARG TARGETARCH
5 |
6 | WORKDIR /app
7 |
8 | COPY go.mod go.sum ./
9 |
10 | RUN go mod download
11 |
12 | COPY . ./
13 |
14 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -v \
15 | -ldflags "-X github.com/AvaProtocol/EigenLayer-AVS/version.semver=$RELEASE_TAG -X github.com/AvaProtocol/EigenLayer-AVS/version.revision=$COMMIT_SHA" \
16 | -o /ava
17 |
18 |
19 | FROM debian:stable-slim
20 |
21 | WORKDIR /app
22 |
23 | RUN useradd -ms /bin/bash ava && \
24 | apt update && apt-get install -y ca-certificates socat telnet
25 |
26 | COPY --from=builder /ava /ava
27 |
28 | ENTRYPOINT ["/ava"]
29 |
--------------------------------------------------------------------------------
/docs/EventTrigger-Debug-Summary.md:
--------------------------------------------------------------------------------
1 | # EventTrigger Expression Not Working - Debug Summary
2 |
3 | ## Problem Statement
4 |
5 | EventTrigger expression returning `null` despite matching transfer events existing:
6 |
7 | ```
8 | Expression: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef&&contracts=[0x7b79995e5f793a07bc00c21412e50ecae098e7f9,0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238]&&from=0xfE66125343Aabda4A330DA667431eC1Acb7BbDA9||to=0xfE66125343Aabda4A330DA667431eC1Acb7BbDA9
9 |
10 | Transaction: https://sepolia.etherscan.io/tx/0x9ff1829f35bd28b0ead18be3c0d9c98dd320e20a87fbad28a6b735d2c7f475cf
11 | Result: null (no aggregator or operator logs)
12 | ```
13 |
14 | ## Debug Steps Added
15 |
16 | I've added comprehensive debug logging to the EventTrigger parsing system. When you run your test again, you should see detailed logs showing exactly what's happening during expression parsing.
17 |
18 | ## Run This to See Debug Output
19 |
20 | 1. **Start your operator/aggregator** with the EventTrigger task
21 | 2. **Look for these log patterns**:
22 |
23 | ```log
24 | 🔍 PARSING ENHANCED EXPRESSION expression="..."
25 | 📋 Expression parts parts=[...] count=3
26 | 🎯 Topic hash topic_hash="0xddf252..."
27 | ✅ Parsed contracts contracts=[...] count=2
28 | ✅ Parsed FROM||TO from="0xfE66..." to="0xfE66..."
29 | ```
30 |
31 | ## What the Debug Logs Will Reveal
32 |
33 | The debug output will show us:
34 | 1. **Is the expression being parsed correctly?**
35 | 2. **Are contracts extracted properly?**
36 | 3. **Are FROM/TO addresses parsed correctly?**
37 | 4. **Is the validation rejecting the expression incorrectly?**
38 |
39 | ## Most Likely Issue
40 |
41 | Based on the code analysis, the problem is likely one of:
42 |
43 | 1. **Expression parsing failure** - The `||` syntax isn't being handled correctly
44 | 2. **Validation rejection** - The filter is being incorrectly marked as "too broad"
45 | 3. **No task registration** - The task isn't being added to the EventTrigger properly
46 |
47 | ## Next Steps
48 |
49 | **Run your test and share the debug logs**. The logs will immediately show us where the problem is occurring, and I can provide a targeted fix.
50 |
51 | The debug logging is now in place in the codebase - just restart your operator/aggregator and run the EventTrigger test again!
--------------------------------------------------------------------------------
/docs/highlevel-diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AvaProtocol/EigenLayer-AVS/d92a8f4b2ce3fb9a2abbfa13c0b2cb446074562d/docs/highlevel-diagram.png
--------------------------------------------------------------------------------
/docs/operator.md:
--------------------------------------------------------------------------------
1 | # Run Operators
2 |
3 | To run the AVS operator, there are 2 steps
4 |
5 | 1. Register to become an EigenLayer operator by following [EigenLayer Operator Guide](https://docs.eigenlayer.xyz/eigenlayer/operator-guides/operator-introduction)
6 | 2. Once become an operator, you can register for Ava Protocol AVS by following the below step
7 |
8 | ### Run Ava Protocol AVS on Holesky testnet
9 |
10 | Download the latest release from https://github.com/AvaProtocol/EigenLayer-AVS/releases for your platform. You can compile for yourself by simply running `go build` at the root level.
11 |
12 | First, Generate Ava Protocol AVS config file. You can put it anywhere. Example `config/operator.yaml` with below content
13 |
14 | ```
15 | # this sets the logger level (true = info, false = debug)
16 | production: true
17 |
18 | operator_address:
19 |
20 | avs_registry_coordinator_address: 0x90c6d6f2A78d5Ce22AB8631Ddb142C03AC87De7a
21 | operator_state_retriever_address: 0xb7bb920538e038DFFEfcB55caBf713652ED2031F
22 |
23 | eth_rpc_url: https://holesky.drpc.org
24 | eth_ws_url: wss://holesky.drpc.org/
25 |
26 | ecdsa_private_key_store_path:
27 | bls_private_key_store_path:
28 |
29 | aggregator_server_ip_port_address: "aggregator-holesky.avaprotocol.org:2206"
30 |
31 | # avs node spec compliance https://eigen.nethermind.io/docs/spec/intro
32 | eigen_metrics_ip_port_address: :9090
33 | enable_metrics: true
34 | node_api_ip_port_address: :9010
35 | enable_node_api: true
36 | ```
37 |
38 | Configure 2 env var for your ECDSA and BLS password. Recall that these are generated when you onboard your operator to EigenLayer. In case your password contains special characters to the command-line, here we export both variables by disabling history expansion temporarily.
39 | ```
40 | set +H
41 | export OPERATOR_BLS_KEY_PASSWORD=""
42 | export OPERATOR_ECDSA_KEY_PASSWORD=""
43 | set -H
44 | ```
45 |
46 | Now, we can start the registration process by running our `ap-avs` AVS release binary.
47 |
48 | ```
49 | ap-avs register --config=./config/operator.yaml
50 | ```
51 |
52 | At the end of process, you should see something like this:
53 |
54 | ```
55 | successfully registered operator with AVS registry coordinator
56 |
57 | Registered operator with avs registry coordinator
58 | ```
59 |
60 | The status can also be checked with `ap-avs status --config=./config/operator.yaml`
61 |
62 | At this point, you're ready to run our operator node by simply do
63 |
64 | ```
65 | ap-avs operator --config=./config/operator.yaml
66 | ```
67 |
68 | # Running operator with docker compose
69 |
70 | To help simplify the process and enable auto update you can use our [official
71 | operator setup repository](https://github.com/AvaProtocol/ap-operator-setup)
72 |
--------------------------------------------------------------------------------
/docs/protocol.md:
--------------------------------------------------------------------------------
1 | # Ava Protocol
2 |
3 | To interact with Ava Protocol, you start by making request to our grpc endpoint. Our protocol is defined inside `protobuf` directory and can code gen client for your language.
4 |
5 | # Endpoint
6 |
7 | ## Prod(Ethereum)
8 |
9 | **aggregator-holesky.avaprotocol.org:2206**
10 |
11 | ## Staging(Holesky)
12 |
13 | **aggregator-holesky.avaprotocol.org:2206**
14 |
15 | ## Local dev
16 |
17 | If using our docker compose, the default port is `2206` so our endpoint is `http://localhost:2206`
18 |
19 | You can interactively develop with grpui on http://localhost:8080
20 |
21 | # Authentication
22 |
23 | To start interacting with our protocol for task management, the process is
24 | generally:
25 |
26 | ## 1. Exchange an `auth token`
27 |
28 | Call `GetKey` method with below data.
29 |
30 | - owner: your wallet address
31 | - expired_at: epoch when your key will be expired
32 | - signature: sign a message in form of `key request for ${wallet.address}
33 | expired at ${expired_at)}`
34 |
35 | The response will have the key which can set on the metadata of subsequent
36 | request. The token will expire at the `expired_at` epoch.
37 |
38 | Please check `examples/signature.js` for reference code on how to generate this
39 | signature.
40 |
41 | # 2. Making GRPC request
42 |
43 | After having auth token, any request that require authentication,set `authkey: ${your-key-from-above}` header in the request.
44 |
45 | Because an account need to send over an auth key generate from the signature
46 | above, no one will be able to view your data and therefore your task and
47 | parameter data will be private.
48 |
49 | # API client
50 |
51 | Using protocol definition in `protobuf` anyone can generate a client use
52 | traditional grpc tool.
53 |
54 | # API Method
55 |
56 | ## Create Task
57 |
58 | ## List An Account Task
59 |
60 | ## Delete Task
61 |
62 | ## Get Smart Account Address
63 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Ava Protocol Examples
2 |
3 | Example codes on how to interact with Ava Protocol RPC server to create and
4 | manage tasks.
5 |
6 | Examples weren't written to be parameterized or extensible. Its only purpose
7 | is to show how to run a specific example, and allow the audience to see
8 | how the code will look like.
9 |
10 | Therefore, the script is harded coded, there is no parameter to provide or anything.
11 |
12 | If you need to change a parameter for a test, edit the code and re-run it.
13 |
14 | # Available example
15 |
16 | ## Prepare depedencies
17 |
18 | ```
19 | npm ci
20 | ```
21 |
22 | Then run:
23 |
24 | ```
25 | node example.js
26 | ```
27 |
28 | it will list all available action to run.
29 |
30 | ## Setting env
31 |
32 | ```
33 | export env=
34 | export PRIVATE_KEY=
35 | ```
36 |
37 | The test example using a dummy token which anyone can mint https://sepolia.etherscan.io/address/0x2e8bdb63d09ef989a0018eeb1c47ef84e3e61f7b#writeProxyContract
38 |
--------------------------------------------------------------------------------
/examples/decode-create-account.js:
--------------------------------------------------------------------------------
1 | import { Interface, getBytes } from "ethers";
2 |
3 | // Define the ABI for the `createAccount` function
4 | const abi = [
5 | "function createAccount(bytes[] owners, uint256 nonce) payable returns (address account)"
6 | ];
7 | const iface = new Interface(abi);
8 |
9 | // Define the call data
10 | const calldata = "0x0ba5ed0c6aa8c49038f819e587e2633c4a9f428a3ffba36f00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040af3f636f8d3e064aed26273c1a453c3abf51c18b54c84b27ce02991aa8d8293b4f5bfa48dda9a26ddf45cc538cb442eb584ab819c4d0684ce390d87e18e554fbe15b0a8c44ecad456533d0110ead2ce0";
11 |
12 | // Decode the call data
13 | const decodedData = iface.decodeFunctionData("createAccount", getBytes("0x" + calldata.substring(42)));
14 |
15 | console.log("Decoded Data:", decodedData);
16 |
--------------------------------------------------------------------------------
/examples/encode-create-account.js:
--------------------------------------------------------------------------------
1 | import { keccak256, toUtf8Bytes } from "ethers";
2 |
3 | // Compute the selector manually
4 | const functionSignature = "createAccount(bytes[],uint256)";
5 | const expectedSelector = keccak256(toUtf8Bytes(functionSignature)).slice(0, 10);
6 | console.log("Expected Selector:", expectedSelector);
7 |
--------------------------------------------------------------------------------
/examples/pack.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("ethers");
2 |
3 | // Your struct values
4 | const tokenAddress = "70EB4D3c164a6B4A5f908D4FBb5a9cAfFb66bAB6";
5 | const weight = "997992210000000000"; // Example value
6 |
7 | // Convert address to 20-byte hex
8 | const encodedToken = ethers.getAddress(tokenAddress);
9 |
10 | // Convert uint96 to 12-byte hex
11 | const encodedWeight = ethers.toBeHex(weight, 12);
12 |
13 | // Concatenate the two encoded values
14 | const packedEncodedData = encodedToken + encodedWeight.slice(2);
15 |
16 | console.log(packedEncodedData);
17 |
18 |
--------------------------------------------------------------------------------
/examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "supports",
3 | "version": "1.0.0",
4 | "description": "misc support utility",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "gen-ts-type": "proto-loader-gen-types --keepCase --longs=String --enums=String --defaults --oneofs --grpcLib=@grpc/grpc-js --outDir=proto/ ../protobuf/*.proto",
9 | "gen-js": "grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./static_codegen/ --grpc_out=grpc_js:./static_codegen/ --proto_path=../protobuf avs.proto"
10 | },
11 | "author": "Ava Protocol",
12 | "license": "ISC",
13 | "dependencies": {
14 | "@grpc/grpc-js": "^1.11.1",
15 | "@grpc/proto-loader": "^0.7.13",
16 | "elliptic": "^6.5.5",
17 | "ethers": "^6.13.2",
18 | "google-protobuf": "^3.21.4",
19 | "grpc-tools": "^1.12.4",
20 | "id128": "^1.6.6",
21 | "keccak256": "^1.0.6",
22 | "lodash": "^4.17.21",
23 | "secp256k1": "^5.0.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/examples/util.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("ethers");
2 |
3 | // Your contract ABI
4 | const abi = [
5 | "function balanceOf(address account) external view returns (uint256)",
6 | "function getRoundData(uint80 _roundId) view returns ( uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)",
7 | "function latestRoundData() public view returns ( uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)",
8 | "function transfer(address _to, uint _value)",
9 | ];
10 |
11 | // Function to encode
12 | const iface = new ethers.Interface(abi);
13 | const call1 = iface.encodeFunctionData("getRoundData", ["18446744073709572839"]);
14 | console.log("Encoded Call1 Data:", call1);
15 | const call2 = iface.encodeFunctionData("balanceOf", ["0xce289bb9fb0a9591317981223cbe33d5dc42268d"]);
16 | console.log("Encoded Call2 Data:", call2);
17 | const call3 = iface.encodeFunctionData("latestRoundData");
18 | console.log("Encoded Call3 Data:", call3);
19 |
20 | const call4 = iface.encodeFunctionData("transfer", ["0xe0f7D11FD714674722d325Cd86062A5F1882E13a", 7621000000000000]);
21 | console.log("Encoded Call4 Data:", call4);
22 |
23 |
--------------------------------------------------------------------------------
/fix_all_trigger_reasons.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import re
4 |
5 | # Read the file
6 | with open('core/taskengine/engine_test.go', 'r') as f:
7 | content = f.read()
8 |
9 | # Pattern to match TriggerReason structures that have BlockNumber but no Type field
10 | # This pattern looks for structures that have BlockNumber but don't already have Type
11 | pattern = r'(&avsproto\.TriggerReason\{\s*BlockNumber: uint64\([^}]+\),\s*\})'
12 |
13 | def replacement_func(match):
14 | # Extract the matched TriggerReason structure
15 | original = match.group(1)
16 |
17 | # Check if it already has Type field
18 | if 'Type:' in original:
19 | return original # Already has Type field, don't modify
20 |
21 | # Add Type field before the closing brace
22 | modified = original.replace('},', ',\n\t\t\tType: avsproto.TriggerReason_Block,\n\t\t},')
23 | return modified
24 |
25 | # Replace all occurrences
26 | content = re.sub(pattern, replacement_func, content, flags=re.MULTILINE | re.DOTALL)
27 |
28 | # Write the file back
29 | with open('core/taskengine/engine_test.go', 'w') as f:
30 | f.write(content)
31 |
32 | print("Fixed all TriggerReason structures in engine_test.go")
--------------------------------------------------------------------------------
/fix_trigger_reason.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import re
4 |
5 | # Read the file
6 | with open('core/taskengine/engine_test.go', 'r') as f:
7 | content = f.read()
8 |
9 | # Pattern to match the old TriggerReason structure
10 | old_pattern = r'Reason: &avsproto\.TriggerReason\{\s*Data: &avsproto\.TriggerReason_Block\{\s*Block: &avsproto\.BlockTriggerData\{\s*BlockNumber: uint64\((\d+)\),\s*\},\s*\},\s*\},'
11 |
12 | # Replacement pattern
13 | new_pattern = r'Reason: &avsproto.TriggerReason{\n\t\t\tBlockNumber: uint64(\1),\n\t\t\tType: avsproto.TriggerReason_Block,\n\t\t},'
14 |
15 | # Replace all occurrences
16 | content = re.sub(old_pattern, new_pattern, content, flags=re.MULTILINE | re.DOTALL)
17 |
18 | # Also fix the test assertions that use GetBlock()
19 | content = re.sub(r'execution\.Reason\.GetBlock\(\)\.BlockNumber', 'execution.Reason.BlockNumber', content)
20 | content = re.sub(r'execution\.Reason\.GetBlock\(\) == nil \|\|', '', content)
21 | content = re.sub(r'execution\.Reason != nil && execution\.Reason\.GetBlock\(\) != nil', 'execution.Reason != nil', content)
22 |
23 | # Write the file back
24 | with open('core/taskengine/engine_test.go', 'w') as f:
25 | f.write(content)
26 |
27 | print("Fixed all TriggerReason structures in engine_test.go")
--------------------------------------------------------------------------------
/fix_trigger_reason.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Fix all TriggerReason structures in engine_test.go
4 | cd /Users/mikasa/Code/EigenLayer-AVS
5 |
6 | # Replace the Data field pattern with the correct structure
7 | perl -i -pe 's/Data: &avsproto\.TriggerReason_Block\{\s*Block: &avsproto\.BlockTriggerData\{\s*BlockNumber: uint64\((\d+)\),\s*\},\s*\},/BlockNumber: uint64($1),\n\t\t\tType: avsproto.TriggerReason_Block,/g' core/taskengine/engine_test.go
8 |
9 | echo "Fixed TriggerReason structures in engine_test.go"
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2024 NAME HERE
3 | */
4 | package main
5 |
6 | import "github.com/AvaProtocol/EigenLayer-AVS/cmd"
7 |
8 | func main() {
9 | cmd.Execute()
10 | }
11 |
--------------------------------------------------------------------------------
/metrics/wrapper.go:
--------------------------------------------------------------------------------
1 | package metrics
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/ethereum/go-ethereum/common"
7 | "github.com/prometheus/client_golang/prometheus"
8 |
9 | "github.com/Layr-Labs/eigensdk-go/logging"
10 | "github.com/Layr-Labs/eigensdk-go/types"
11 | )
12 |
13 | type MetricsOnlyLogger struct {
14 | logging.Logger
15 | }
16 |
17 | func (l *MetricsOnlyLogger) Error(msg string, keysAndValues ...interface{}) {
18 | l.Logger.Error(fmt.Sprintf("[METRICS ONLY] %s", msg), keysAndValues...)
19 | }
20 |
21 | func (l *MetricsOnlyLogger) Errorf(format string, args ...interface{}) {
22 | l.Logger.Errorf("[METRICS ONLY] "+format, args...)
23 | }
24 |
25 | type EconomicMetricsCollector struct {
26 | elReader interface{}
27 | avsReader interface{}
28 | avsName string
29 | logger logging.Logger
30 | operatorAddr common.Address
31 | quorumNames map[types.QuorumNum]string
32 |
33 | operatorStake *prometheus.GaugeVec
34 | }
35 |
36 | func NewMetricsOnlyEconomicCollector(
37 | elReader interface{},
38 | avsReader interface{},
39 | avsName string,
40 | logger logging.Logger,
41 | operatorAddr common.Address,
42 | quorumNames map[types.QuorumNum]string,
43 | ) prometheus.Collector {
44 | wrappedLogger := &MetricsOnlyLogger{
45 | Logger: logger,
46 | }
47 |
48 | operatorStake := prometheus.NewGaugeVec(
49 | prometheus.GaugeOpts{
50 | Namespace: "eigenlayer",
51 | Subsystem: "economic",
52 | Name: "operator_stake",
53 | Help: "Operator stake in the EigenLayer protocol",
54 | },
55 | []string{"quorum"},
56 | )
57 |
58 | return &EconomicMetricsCollector{
59 | elReader: elReader,
60 | avsReader: avsReader,
61 | avsName: avsName,
62 | logger: wrappedLogger, // Use the wrapped logger
63 | operatorAddr: operatorAddr,
64 | quorumNames: quorumNames,
65 | operatorStake: operatorStake,
66 | }
67 | }
68 |
69 | func (c *EconomicMetricsCollector) Describe(ch chan<- *prometheus.Desc) {
70 | c.operatorStake.Describe(ch)
71 | }
72 |
73 | func (c *EconomicMetricsCollector) Collect(ch chan<- prometheus.Metric) {
74 |
75 | for quorumNum, quorumName := range c.quorumNames {
76 |
77 | c.logger.Error("Failed to get operator stake",
78 | "err", "Failed to get operator stake: 500 Internal Server Error: {\"id\":143,\"jsonrpc\":\"2.0\",\"error\":{\"message\":\"Unknown block\",\"code\":26}}",
79 | "quorum", quorumName,
80 | "quorumNum", quorumNum,
81 | "operatorAddr", c.operatorAddr.Hex(),
82 | )
83 |
84 | c.operatorStake.WithLabelValues(quorumName).Set(0)
85 | }
86 |
87 | c.operatorStake.Collect(ch)
88 | }
89 |
--------------------------------------------------------------------------------
/migrations/20250603-183034-requiredfield-datastructure.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/AvaProtocol/EigenLayer-AVS/storage"
8 | )
9 |
10 | // TokenMetadataFieldsMigration handles the addition of new required fields to TokenMetadata struct
11 | func TokenMetadataFieldsMigration(db storage.Storage) (int, error) {
12 | log.Printf("Starting migration: TokenMetadata required fields migration")
13 |
14 | // This migration handles the addition of new TokenMetadata struct fields:
15 | // - TokenMetadata.Address: Added required field of type string
16 | // - TokenMetadata.Name: Added required field of type string
17 | // - TokenMetadata.Symbol: Added required field of type string
18 | // - TokenMetadata.Decimals: Added required field of type uint32
19 | // - TokenMetadata.Source: Added required field of type string
20 |
21 | recordsUpdated := 0
22 |
23 | // Since these are new structs (TokenMetadata, TokenEnrichmentService, TriggerData)
24 | // being added to the codebase, there shouldn't be existing data to migrate.
25 | // However, we should verify this assumption by checking if any related keys exist.
26 |
27 | // Check for any existing token-related keys that might need migration
28 | tokenKeys, err := db.ListKeys("token:*")
29 | if err != nil {
30 | return 0, fmt.Errorf("failed to list token keys: %w", err)
31 | }
32 |
33 | if len(tokenKeys) > 0 {
34 | log.Printf("Found %d existing token-related keys that may need review", len(tokenKeys))
35 | // If there are existing token keys, they would need to be updated
36 | // to include the new required fields with appropriate default values
37 | for _, key := range tokenKeys {
38 | log.Printf("Reviewing key: %s", key)
39 | // TODO: If needed, implement specific migration logic for existing token data
40 | }
41 | }
42 |
43 | // Check for any enrichment-related keys
44 | enrichmentKeys, err := db.ListKeys("enrichment:*")
45 | if err != nil {
46 | return 0, fmt.Errorf("failed to list enrichment keys: %w", err)
47 | }
48 |
49 | if len(enrichmentKeys) > 0 {
50 | log.Printf("Found %d existing enrichment-related keys that may need review", len(enrichmentKeys))
51 | }
52 |
53 | log.Printf("Migration completed successfully. %d records were updated.", recordsUpdated)
54 | return recordsUpdated, nil
55 | }
56 |
--------------------------------------------------------------------------------
/migrations/README.md:
--------------------------------------------------------------------------------
1 | # Database Migrations
2 |
3 | This document outlines how to create and manage database migrations for the EigenLayer-AVS project.
4 |
5 | ## What are Migrations?
6 |
7 | Migrations are a way to make incremental, reversible changes to the database schema or data. They help maintain database consistency across different environments and versions of the application.
8 |
9 | ## Creating a New Migration
10 |
11 | To create a new migration, follow these steps:
12 |
13 | 1. Create a new Go file in the `migrations` package with a descriptive name (e.g., `my_migration.go`)
14 |
15 | 2. Define your migration function that implements the required signature:
16 |
17 | ```go
18 | func YourMigrationName(db storage.Storage) error {
19 | // Migration logic here
20 | // Use db.Put(), db.Get(), db.Delete(), etc. to modify data
21 |
22 | return nil // Return error if migration fails
23 | }
24 | ```
25 |
26 | 3. Register your migration in the `migrations.go` file by adding it to the `Migrations` slice:
27 |
28 | ```go
29 | var Migrations = []migrator.Migration{
30 | // Existing migrations...
31 | {
32 | Name: "your-migration-name",
33 | Function: YourMigrationName,
34 | },
35 | }
36 | ```
37 |
38 | ## Migration Best Practices
39 |
40 | 1. **Descriptive Names**: Use clear, descriptive names for your migrations that indicate what they do.
41 |
42 | 2. **Idempotency**: Migrations should be idempotent (can be run multiple times without side effects).
43 |
44 | 3. **Atomicity**: Each migration should represent a single, atomic change to the database.
45 |
46 | 4. **Error Handling**: Properly handle errors and return them to the migrator.
47 |
48 | 5. **Documentation**: Add comments to your migration code explaining what it does and why.
49 |
50 | ## Example Migration
51 |
52 | An example of migration can be view in function `ChangeEpochToMs`
53 |
--------------------------------------------------------------------------------
/migrations/migrations.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "github.com/AvaProtocol/EigenLayer-AVS/core/migrator"
5 | )
6 |
7 | // Migrations contains the list of database migrations to be applied
8 | // We keep this as an empty array after migrations are applied to avoid complexity
9 | // Any future migrations should be applied manually or through separate tooling
10 | var Migrations = []migrator.Migration{
11 | // {
12 | // // The name of the migration will be recored in our key-value store, and it's sorted lexicographically
13 | // // so we can use the timestamp to sort the migrations in the right order for debugging
14 | // // We should prefix the name with the timestamp in format of YYYYMMDD-HHMMSS
15 | // // Not a requirement but strongly recommended
16 | // Name: "20250405-232000-change-epoch-to-ms",
17 | // Function: ChangeEpochToMs,
18 | // },
19 | // Token metadata fields migration - adding required fields to new TokenMetadata struct
20 | {
21 | Name: "20250603-183034-token-metadata-fields",
22 | Function: TokenMetadataFieldsMigration,
23 | },
24 | // Each migration should be added to this list
25 | }
26 |
--------------------------------------------------------------------------------
/model/secret.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | type Secret struct {
4 | Name string `validate:"required"`
5 | Value string `validate:"required"`
6 |
7 | // User is the original EOA that create the secret
8 | User *User `validate:"required"`
9 |
10 | // We support 3 scopes currently
11 | // - org
12 | // - user
13 | // - workflow
14 | Scope string `validate:"oneof=user org workflow"`
15 |
16 | OrgID string
17 | WorkflowID string
18 | }
19 |
--------------------------------------------------------------------------------
/model/user.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "math/big"
7 |
8 | "github.com/AvaProtocol/EigenLayer-AVS/core/chainio/aa"
9 | "github.com/ethereum/go-ethereum/common"
10 | "github.com/ethereum/go-ethereum/ethclient"
11 | )
12 |
13 | type User struct {
14 | Address common.Address
15 | SmartAccountAddress *common.Address
16 | }
17 |
18 | func (u *User) LoadDefaultSmartWallet(rpcClient *ethclient.Client) error {
19 | smartAccountAddress, err := aa.GetSenderAddress(rpcClient, u.Address, big.NewInt(0))
20 | if err != nil {
21 | return fmt.Errorf("Rpc error")
22 | }
23 | u.SmartAccountAddress = smartAccountAddress
24 | return nil
25 | }
26 |
27 | // Return the smartwallet struct re-present the default wallet for this user
28 | func (u *User) ToSmartWallet() *SmartWallet {
29 | return &SmartWallet{
30 | Owner: &u.Address,
31 | Address: u.SmartAccountAddress,
32 | }
33 | }
34 |
35 | type SmartWallet struct {
36 | Owner *common.Address `json:"owner"`
37 | Address *common.Address `json:"address"`
38 | Factory *common.Address `json:"factory,omitempty"`
39 | Salt *big.Int `json:"salt"`
40 | IsHidden bool `json:"is_hidden,omitempty"`
41 | }
42 |
43 | func (w *SmartWallet) ToJSON() ([]byte, error) {
44 | return json.Marshal(w)
45 | }
46 |
47 | func (w *SmartWallet) FromStorageData(body []byte) error {
48 | err := json.Unmarshal(body, w)
49 |
50 | return err
51 | }
52 |
53 | type SmartWalletTaskStat struct {
54 | Total uint64
55 | Active uint64
56 | Completed uint64
57 | Failed uint64
58 | Canceled uint64
59 | }
60 |
--------------------------------------------------------------------------------
/operator/envs.go:
--------------------------------------------------------------------------------
1 | package operator
2 |
3 | import (
4 | "fmt"
5 | "math/big"
6 |
7 | "github.com/ethereum/go-ethereum/common"
8 | )
9 |
10 | // Default contract addresses for Ethereum mainnet (chainId = 1)
11 | // These addresses are specific to Ethereum mainnet and should not be used on other networks
12 | const (
13 | // Ethereum mainnet AVS Registry Coordinator address
14 | MainnetAVSRegistryCoordinatorAddress = "0x8DE3Ee0dE880161Aa0CD8Bf9F8F6a7AfEeB9A44B"
15 | // Ethereum mainnet Operator State Retriever address
16 | MainnetOperatorStateRetrieverAddress = "0xb3af70D5f72C04D1f490ff49e5aB189fA7122713"
17 | )
18 |
19 | // Populate configuration based on known env
20 | // TODO: We can fetch this dynamically from aggregator so we can upgrade the
21 | // config without the need to release operator
22 | var (
23 | mainnetChainID = big.NewInt(1)
24 | )
25 |
26 | // Populate config based on chain id.
27 | // There is a certain config that is depend based on env
28 | func (o *Operator) PopulateKnownConfigByChainID(chainID *big.Int) error {
29 | if chainID.Cmp(mainnetChainID) == 0 {
30 | // Ethereum mainnet configuration
31 | o.apConfigAddr = common.HexToAddress("0x9c02dfc92eea988902a98919bf4f035e4aaefced")
32 |
33 | // Apply default contract addresses for mainnet if not provided
34 | if o.config.AVSRegistryCoordinatorAddress == "" {
35 | o.config.AVSRegistryCoordinatorAddress = MainnetAVSRegistryCoordinatorAddress
36 | o.logger.Warnf("AVS Registry Coordinator Address not provided for mainnet, using default: %s", o.config.AVSRegistryCoordinatorAddress)
37 | }
38 | if o.config.OperatorStateRetrieverAddress == "" {
39 | o.config.OperatorStateRetrieverAddress = MainnetOperatorStateRetrieverAddress
40 | o.logger.Warnf("Operator State Retriever Address not provided for mainnet, using default: %s", o.config.OperatorStateRetrieverAddress)
41 | }
42 |
43 | if o.config.TargetChain.EthRpcUrl == "" {
44 | o.config.TargetChain.EthRpcUrl = o.config.EthRpcUrl
45 | o.config.TargetChain.EthWsUrl = o.config.EthWsUrl
46 | }
47 | } else {
48 | // Testnet configuration
49 | o.apConfigAddr = common.HexToAddress("0xb8abbb082ecaae8d1cd68378cf3b060f6f0e07eb")
50 |
51 | // For testnets, we don't have default addresses - they must be provided in config
52 | if o.config.AVSRegistryCoordinatorAddress == "" {
53 | o.logger.Errorf("AVS Registry Coordinator Address is required for testnet (chainId: %s)", chainID.String())
54 | return fmt.Errorf("AVS Registry Coordinator Address is required for testnet (chainId: %s)", chainID.String())
55 | }
56 | if o.config.OperatorStateRetrieverAddress == "" {
57 | o.logger.Errorf("Operator State Retriever Address is required for testnet (chainId: %s)", chainID.String())
58 | return fmt.Errorf("Operator State Retriever Address is required for testnet (chainId: %s)", chainID.String())
59 | }
60 | }
61 |
62 | o.logger.Infof("Chain configuration applied for chainId: %s", chainID.String())
63 | return nil
64 | }
65 |
--------------------------------------------------------------------------------
/operator/password.go:
--------------------------------------------------------------------------------
1 | package operator
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | )
7 |
8 | // lookup and return passphrase from env var. panic to fail fast if a passphrase
9 | // isn't existed in the env
10 | func loadECDSAPassword() string {
11 | passphrase, ok := os.LookupEnv("OPERATOR_ECDSA_KEY_PASSWORD")
12 | if !ok {
13 | panic(fmt.Errorf("missing OPERATOR_ECDSA_KEY_PASSWORD env var"))
14 | }
15 |
16 | if passphrase == "" {
17 | panic("passphrase is empty. pleae make sure you define OPERATOR_ECDSA_KEY_PASSWORD")
18 | }
19 |
20 | return passphrase
21 | }
22 |
--------------------------------------------------------------------------------
/operator/process_message.go:
--------------------------------------------------------------------------------
1 | package operator
2 |
3 | import (
4 | avspb "github.com/AvaProtocol/EigenLayer-AVS/protobuf"
5 | )
6 |
7 | func (o *Operator) processMessage(resp *avspb.SyncMessagesResp) {
8 | switch resp.Op {
9 | case avspb.MessageOp_CancelTask, avspb.MessageOp_DeleteTask:
10 | o.logger.Info("removing task from all triggers", "task_id", resp.TaskMetadata.TaskId, "operation", resp.Op)
11 | o.eventTrigger.RemoveCheck(resp.TaskMetadata.TaskId)
12 | o.blockTrigger.RemoveCheck(resp.TaskMetadata.TaskId)
13 | o.timeTrigger.RemoveCheck(resp.TaskMetadata.TaskId)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/byte4/signature.go:
--------------------------------------------------------------------------------
1 | package byte4
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/ethereum/go-ethereum/accounts/abi"
9 | "github.com/ethereum/go-ethereum/crypto"
10 | )
11 |
12 | // GetMethodFromCalldata returns the method name and ABI method for a given 4-byte selector or full calldata
13 | func GetMethodFromCalldata(parsedABI abi.ABI, selector []byte) (*abi.Method, error) {
14 | if len(selector) < 4 {
15 | return nil, fmt.Errorf("invalid selector length: %d", len(selector))
16 | }
17 |
18 | // Get first 4 bytes of the calldata. This is the first 8 characters of the calldata
19 | // Function calls in the Ethereum Virtual Machine(EVM) are specified by the first four bytes of data sent with a transaction. These 4-byte signatures are defined as the first four bytes of the Keccak hash (SHA3) of the canonical representation of the function signature.
20 |
21 | methodID := selector[:4]
22 |
23 | // Find matching method in ABI
24 | for name, method := range parsedABI.Methods {
25 | // Build the signature string from inputs
26 | var types []string
27 | for _, input := range method.Inputs {
28 | types = append(types, input.Type.String())
29 | }
30 |
31 | // Create method signature: name(type1,type2,...)
32 | sig := fmt.Sprintf("%v(%v)", name, strings.Join(types, ","))
33 | hash := crypto.Keccak256([]byte(sig))[:4]
34 |
35 | if bytes.Equal(hash, methodID) {
36 | return &method, nil
37 | }
38 | }
39 |
40 | return nil, fmt.Errorf("no matching method found for selector: 0x%x", methodID)
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/eip1559/eip1559.go:
--------------------------------------------------------------------------------
1 | package eip1559
2 |
3 | import (
4 | "context"
5 | "math/big"
6 |
7 | "github.com/ethereum/go-ethereum/ethclient"
8 | )
9 |
10 | func SuggestFee(client *ethclient.Client) (*big.Int, *big.Int, error) {
11 | // Get suggested gas tip cap (maxPriorityFeePerGas)
12 | tipCap, err := client.SuggestGasTipCap(context.Background())
13 | if err != nil {
14 | return nil, nil, err
15 | }
16 |
17 | // Estimate base fee for the next block
18 | header, err := client.HeaderByNumber(context.Background(), nil)
19 | if err != nil {
20 | return nil, nil, err
21 | }
22 | buffer := new(big.Int).Div(
23 | tipCap,
24 | big.NewInt(100),
25 | )
26 | buffer = new(big.Int).Mul(
27 | buffer,
28 | big.NewInt(13),
29 | )
30 |
31 | maxPriorityFeePerGas := new(big.Int).Add(tipCap, buffer)
32 |
33 | var maxFeePerGas *big.Int
34 |
35 | baseFee := header.BaseFee
36 | if baseFee != nil {
37 | maxFeePerGas = new(big.Int).Add(
38 | new(big.Int).Mul(baseFee, big.NewInt(2)),
39 | maxPriorityFeePerGas,
40 | )
41 | } else {
42 | maxFeePerGas = new(big.Int).Set(maxPriorityFeePerGas)
43 | }
44 |
45 | return maxFeePerGas, maxPriorityFeePerGas, nil
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/erc20/Makefile:
--------------------------------------------------------------------------------
1 | genabi:
2 | abigen --abi=erc20.abi --pkg=erc20 --out=erc20.go
3 |
--------------------------------------------------------------------------------
/pkg/erc4337/bundler/gas.go:
--------------------------------------------------------------------------------
1 | package bundler
2 |
3 | import "math/big"
4 |
5 | type GasEstimation struct {
6 | PreVerificationGas *big.Int
7 | VerificationGasLimit *big.Int
8 | CallGasLimit *big.Int
9 | VerificationGas *big.Int
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/erc4337/bundler/userop.go:
--------------------------------------------------------------------------------
1 | package bundler
2 |
3 | import (
4 | "github.com/ethereum/go-ethereum/common"
5 | )
6 |
7 | // UserOperation represents an EIP-4337 style transaction for a smart contract account.
8 | type UserOperation struct {
9 | Sender common.Address `json:"sender"`
10 | Nonce string `json:"nonce"`
11 | InitCode string `json:"initCode"`
12 | CallData string `json:"callData"`
13 | CallGasLimit string `json:"callGasLimit"`
14 | VerificationGasLimit string `json:"verificationGasLimit"`
15 | PreVerificationGas string `json:"preVerificationGas"`
16 | MaxFeePerGas string `json:"maxFeePerGas"`
17 | MaxPriorityFeePerGas string `json:"maxPriorityFeePerGas"`
18 | PaymasterAndData string `json:"paymasterAndData"`
19 | Signature string `json:"signature"`
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/erc4337/userop/parse.go:
--------------------------------------------------------------------------------
1 | // Source: https://github.com/stackup-wallet/stackup-bundler/tree/main
2 | package userop
3 |
4 | import (
5 | "encoding/hex"
6 | "errors"
7 | "fmt"
8 | "math/big"
9 | "reflect"
10 | "sync"
11 |
12 | "github.com/ethereum/go-ethereum/common"
13 | validator "github.com/go-playground/validator/v10"
14 | "github.com/mitchellh/mapstructure"
15 | )
16 |
17 | var (
18 | validate = validator.New()
19 | onlyOnce = sync.Once{}
20 |
21 | ErrBadUserOperationData = errors.New("cannot decode UserOperation")
22 | )
23 |
24 | func exactFieldMatch(mapKey, fieldName string) bool {
25 | return mapKey == fieldName
26 | }
27 |
28 | func decodeOpTypes(
29 | f reflect.Kind,
30 | t reflect.Kind,
31 | data interface{}) (interface{}, error) {
32 | // String to common.Address conversion
33 | if f == reflect.String && t == reflect.Array {
34 | addrStr, ok := data.(string)
35 | if !ok {
36 | return nil, errors.New("expected string for address conversion")
37 | }
38 | return common.HexToAddress(addrStr), nil
39 | }
40 |
41 | // String to big.Int conversion
42 | if f == reflect.String && t == reflect.Struct {
43 | n := new(big.Int)
44 | var ok bool
45 | dataStr, ok := data.(string)
46 | if !ok {
47 | return nil, errors.New("expected string for bigInt conversion")
48 | }
49 | n, ok = n.SetString(dataStr, 0)
50 | if !ok {
51 | return nil, errors.New("bigInt conversion failed")
52 | }
53 | return n, nil
54 | }
55 |
56 | // Float64 to big.Int conversion
57 | if f == reflect.Float64 && t == reflect.Struct {
58 | n, ok := data.(float64)
59 | if !ok {
60 | return nil, errors.New("bigInt conversion failed")
61 | }
62 | return big.NewInt(int64(n)), nil
63 | }
64 |
65 | // String to []byte conversion
66 | if f == reflect.String && t == reflect.Slice {
67 | byteStr, ok := data.(string)
68 | if !ok {
69 | return nil, errors.New("expected string for byte conversion")
70 | }
71 | if len(byteStr) < 2 || byteStr[:2] != "0x" {
72 | return nil, errors.New("not byte string")
73 | }
74 |
75 | b, err := hex.DecodeString(byteStr[2:])
76 | if err != nil {
77 | return nil, err
78 | }
79 | return b, nil
80 | }
81 |
82 | return data, nil
83 | }
84 |
85 | func validateAddressType(field reflect.Value) interface{} {
86 | value, ok := field.Interface().(common.Address)
87 | if !ok || value == common.HexToAddress("0x") {
88 | return nil
89 | }
90 |
91 | return field
92 | }
93 |
94 | func validateBigIntType(field reflect.Value) interface{} {
95 | value, ok := field.Interface().(big.Int)
96 | if !ok || value.Cmp(big.NewInt(0)) == -1 {
97 | return nil
98 | }
99 |
100 | return field
101 | }
102 |
103 | // New decodes a map into a UserOperation object and validates all the fields are correctly typed.
104 | func New(data map[string]any) (*UserOperation, error) {
105 | var op UserOperation
106 |
107 | // Convert map to struct
108 | config := &mapstructure.DecoderConfig{
109 | DecodeHook: decodeOpTypes,
110 | Result: &op,
111 | ErrorUnset: true,
112 | MatchName: exactFieldMatch,
113 | }
114 | decoder, err := mapstructure.NewDecoder(config)
115 | if err != nil {
116 | return nil, err
117 | }
118 | if err := decoder.Decode(data); err != nil {
119 | return nil, fmt.Errorf("%w: %w", ErrBadUserOperationData, err)
120 | }
121 |
122 | // Validate struct
123 | onlyOnce.Do(func() {
124 | validate.RegisterCustomTypeFunc(validateAddressType, common.Address{})
125 | validate.RegisterCustomTypeFunc(validateBigIntType, big.Int{})
126 | })
127 | err = validate.Struct(op)
128 | if err != nil {
129 | return nil, err
130 | }
131 |
132 | return &op, nil
133 | }
134 |
--------------------------------------------------------------------------------
/pkg/graphql/graphql_test.go:
--------------------------------------------------------------------------------
1 | package graphql
2 |
3 | import (
4 | "context"
5 | "strings"
6 | "testing"
7 | )
8 |
9 | func TestSimpleQuery(t *testing.T) {
10 | sb := &strings.Builder{}
11 | log := func(s string) {
12 | sb.WriteString(s)
13 | }
14 |
15 | endpoint := "https://spacex-production.up.railway.app/"
16 | client, _ := NewClient(endpoint, log)
17 |
18 | query := `
19 | query Rockets {
20 | rockets(limit: 2, ) {
21 | id
22 | name
23 | }
24 | ships(limit: 3, sort: "ID") {
25 | id
26 | name
27 | }
28 | }
29 | `
30 |
31 | type responseStruct struct {
32 | Rockets []struct {
33 | ID string `json:"id"`
34 | Name string `json:"name"`
35 | } `json:"rockets"`
36 | Ships []struct {
37 | ID string `json:"id"`
38 | Name string `json:"name"`
39 | } `json:"ships"`
40 | }
41 |
42 | var resp responseStruct
43 | req := NewRequest(query)
44 | err := client.Run(context.Background(), req, &resp)
45 | if err != nil {
46 | t.Fatalf("query failed: %v", err)
47 | }
48 |
49 | // Check lengths
50 | lengthTests := []struct {
51 | name string
52 | got int
53 | want int
54 | category string
55 | }{
56 | {
57 | name: "rockets count",
58 | got: len(resp.Rockets),
59 | want: 2,
60 | category: "rockets",
61 | },
62 | {
63 | name: "ships count",
64 | got: len(resp.Ships),
65 | want: 3,
66 | category: "ships",
67 | },
68 | }
69 |
70 | for _, tt := range lengthTests {
71 | t.Run(tt.name, func(t *testing.T) {
72 | if tt.got != tt.want {
73 | t.Errorf("expected exactly %d %s got %d", tt.want, tt.category, tt.got)
74 | }
75 | })
76 | }
77 |
78 | // Check rocket details
79 | rocketTests := []struct {
80 | name string
81 | index int
82 | wantID string
83 | wantName string
84 | }{
85 | {
86 | name: "first rocket",
87 | index: 0,
88 | wantID: "5e9d0d95eda69955f709d1eb",
89 | wantName: "Falcon 1",
90 | },
91 | {
92 | name: "second rocket",
93 | index: 1,
94 | wantID: "5e9d0d95eda69973a809d1ec",
95 | wantName: "Falcon 9",
96 | },
97 | }
98 |
99 | for _, tt := range rocketTests {
100 | t.Run(tt.name, func(t *testing.T) {
101 | rocket := resp.Rockets[tt.index]
102 | if rocket.ID != tt.wantID {
103 | t.Errorf("expected rocket ID %s, got %s", tt.wantID, rocket.ID)
104 | }
105 | if rocket.Name != tt.wantName {
106 | t.Errorf("expected rocket name %s, got %s", tt.wantName, rocket.Name)
107 | }
108 | })
109 | }
110 |
111 | // Check ship details
112 | shipTests := []struct {
113 | name string
114 | index int
115 | wantID string
116 | wantName string
117 | }{
118 | {
119 | name: "first ship",
120 | index: 0,
121 | wantID: "5ea6ed2d080df4000697c901",
122 | wantName: "American Champion",
123 | },
124 | {
125 | name: "second ship",
126 | index: 1,
127 | wantID: "5ea6ed2d080df4000697c902",
128 | wantName: "American Islander",
129 | },
130 | {
131 | name: "third ship",
132 | index: 2,
133 | wantID: "5ea6ed2d080df4000697c903",
134 | wantName: "American Spirit",
135 | },
136 | }
137 |
138 | for _, tt := range shipTests {
139 | t.Run(tt.name, func(t *testing.T) {
140 | ship := resp.Ships[tt.index]
141 | if ship.ID != tt.wantID {
142 | t.Errorf("expected ship ID %s, got %s", tt.wantID, ship.ID)
143 | }
144 | if ship.Name != tt.wantName {
145 | t.Errorf("expected ship name %s, got %s", tt.wantName, ship.Name)
146 | }
147 | })
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/pkg/ipfetcher/ipfetcher.go:
--------------------------------------------------------------------------------
1 | package ipfetcher
2 |
3 | import (
4 | "io"
5 | "net"
6 | "net/http"
7 | "strings"
8 | "time"
9 | )
10 |
11 | // GetIP fetches the public IP address from icanhazip.com
12 | func GetIP() (string, error) {
13 | // Create a custom HTTP client with timeout settings
14 | client := &http.Client{
15 | Timeout: 30 * time.Second,
16 | Transport: &http.Transport{
17 | DialContext: (&net.Dialer{
18 | Timeout: 10 * time.Second,
19 | }).DialContext,
20 | TLSHandshakeTimeout: 10 * time.Second,
21 | },
22 | }
23 |
24 | // Make the GET request
25 | resp, err := client.Get("https://icanhazip.com")
26 | if err != nil {
27 | return "", err
28 | }
29 | defer resp.Body.Close()
30 |
31 | // Read the response body
32 | body, err := io.ReadAll(resp.Body)
33 | if err != nil {
34 | return "", err
35 | }
36 |
37 | // Trim any surrounding whitespace from the response body
38 | ip := strings.TrimSpace(string(body))
39 | return ip, nil
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/timekeeper/timekeeper.go:
--------------------------------------------------------------------------------
1 | package timekeeper
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | )
7 |
8 | type ElapsingStatus int
9 |
10 | const (
11 | Running ElapsingStatus = 1
12 | Pause ElapsingStatus = 2
13 | )
14 |
15 | type Elapsing struct {
16 | checkpoint time.Time
17 |
18 | carryOn time.Duration
19 |
20 | status ElapsingStatus
21 | }
22 |
23 | func NewElapsing() *Elapsing {
24 | elapse := &Elapsing{
25 | // In Go, Now keeps track both of wallclock and monotonic clock
26 | // therefore we can use it to check delta as well
27 | checkpoint: time.Now(),
28 |
29 | status: Running,
30 | }
31 |
32 | return elapse
33 | }
34 |
35 | func (e *Elapsing) Pause() error {
36 | if e.status == Pause {
37 | return fmt.Errorf("elapsing is pause already")
38 | }
39 |
40 | e.carryOn = e.Report()
41 | e.status = Pause
42 |
43 | return nil
44 | }
45 |
46 | func (e *Elapsing) Resume() error {
47 | if e.status != Pause {
48 | return fmt.Errorf("elapsing is not pause")
49 | }
50 |
51 | e.checkpoint = time.Now()
52 | e.status = Running
53 |
54 | return nil
55 | }
56 |
57 | func (e *Elapsing) Reset() error {
58 | e.status = Running
59 | e.carryOn = 0
60 | e.checkpoint = time.Now()
61 |
62 | return nil
63 | }
64 |
65 | func (e *Elapsing) Report() time.Duration {
66 | if e.status == Pause {
67 | return time.Duration(0)
68 | }
69 |
70 | now := time.Now()
71 | total := now.Sub(e.checkpoint) + e.carryOn
72 |
73 | e.carryOn = time.Duration(0)
74 | e.checkpoint = now
75 |
76 | return total
77 | }
78 |
--------------------------------------------------------------------------------
/pkg/timekeeper/timekeeper_test.go:
--------------------------------------------------------------------------------
1 | package timekeeper
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | func TestElapsing(t *testing.T) {
9 | elapse := NewElapsing()
10 |
11 | time.Sleep(50 * time.Millisecond)
12 |
13 | d1 := elapse.Report()
14 | if d1 <= 50*time.Millisecond {
15 | t.Errorf("elapse time is wrong. expect some amount > 50ms, got %s", d1)
16 | }
17 |
18 | elapse.Pause()
19 |
20 | time.Sleep(50 * time.Millisecond)
21 | elapse.Resume()
22 | d2 := elapse.Report()
23 |
24 | if d2 >= 1*time.Millisecond {
25 | t.Errorf("elapse time is wrong. expect some amount > 50ms, got %s", d2)
26 | }
27 | }
28 |
29 | func TestReset(t *testing.T) {
30 | elapse := NewElapsing()
31 |
32 | time.Sleep(50 * time.Millisecond)
33 |
34 | elapse.Reset()
35 | d1 := elapse.Report()
36 | if d1 >= 1*time.Millisecond {
37 | t.Errorf("elapse time is wrong. expect some amount ~1us, got %s", d1)
38 | }
39 | }
40 |
41 | func TestCarryon(t *testing.T) {
42 | elapse := NewElapsing()
43 | time.Sleep(10 * time.Millisecond)
44 | elapse.Pause()
45 |
46 | time.Sleep(10 * time.Millisecond)
47 | elapse.Resume()
48 | d1 := elapse.Report()
49 |
50 | if d1 >= 20*time.Millisecond {
51 | t.Errorf("elapse time is wrong. expect some amount ~10ms, got %s", d1)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/protobuf/export.go:
--------------------------------------------------------------------------------
1 | package avsproto
2 |
3 | // Export the oneof so we can type outside of this package
4 | // https://github.com/golang/protobuf/issues/261#issuecomment-430496210
5 | type IsExecution_Step_OutputData = isExecution_Step_OutputData
6 |
--------------------------------------------------------------------------------
/token_whitelist/base-sepolia.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Tea Token",
4 | "symbol": "TEA",
5 | "address": "0x87C51CD469A0E1E2aF0e0e597fD88D9Ae4BaA967"
6 | },
7 | {
8 | "name": "Wrapped fake liquid staked Ether 2.0",
9 | "symbol": "wstETH",
10 | "address": "0x13e5FB0B6534BB22cBC59Fae339dbBE0Dc906871"
11 | },
12 | {
13 | "name": "Circle Fake USD",
14 | "symbol": "USDC",
15 | "address": "0xf7464321dE37BdE4C03AAeeF6b1e7b71379A9a64"
16 | },
17 | {
18 | "name": "Staked TEA",
19 | "symbol": "stTEA",
20 | "address": "0xa8cB1964ea7f9674Ac6EC2Bc87D386380bE264F8"
21 | },
22 | {
23 | "name": "Bond ETH",
24 | "symbol": "bondETH",
25 | "address": "0x5Bd36745f6199CF32d2465Ef1F8D6c51dCA9BdEE"
26 | },
27 | {
28 | "name": "Levered ETH",
29 | "symbol": "levETH",
30 | "address": "0x98f665D98a046fB81147879eCBE9A6fF68BC276C"
31 | },
32 | {
33 | "name": "SOGNI testnet token V1.0.2",
34 | "symbol": "tSOGNI",
35 | "address": "0xF0593d8dBb5D443156F782d89C6978CB4D8205D6"
36 | },
37 | {
38 | "name": "Bond ETH",
39 | "symbol": "BondETH",
40 | "address": "0x3EB4b2c7D235fE915E3A0eF6BE73FD458Bb891F4"
41 | },
42 | {
43 | "name": "USDG",
44 | "symbol": "USDG",
45 | "address": "0xD4fA4dE9D8F8DB39EAf4de9A19bF6910F6B5bD60"
46 | },
47 | {
48 | "name": "Leverage ETH",
49 | "symbol": "LevETH",
50 | "address": "0x8EE92Ce1caF5848d7a54672fC4320E4f92748643"
51 | },
52 | {
53 | "name": "Angry Dynomites Lab - Badges",
54 | "symbol": "BADGES",
55 | "address": "0x187F4cF75d86810fC9c9dDa1bc4B6Fd86c98158B"
56 | },
57 | {
58 | "name": "Black Pass",
59 | "symbol": "BP",
60 | "address": "0x5878e492fba20F47884841d093b79d259B5B799B"
61 | },
62 | {
63 | "name": "Coinbase Wrapped Fake Staked ETH",
64 | "symbol": "cbETH",
65 | "address": "0x1197766B82Eee9c2e57674E53F0D961590e43769"
66 | },
67 | {
68 | "name": "USDC",
69 | "symbol": "USDC",
70 | "address": "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
71 | },
72 | {
73 | "name": "Bond ETH",
74 | "symbol": "bondETH",
75 | "address": "0x1aC493C87a483518642f320Ba5b342c7b78154ED"
76 | },
77 | {
78 | "name": "Ethereum",
79 | "symbol": "ETH",
80 | "address": "0x464C8ec100F2F42fB4e42E07E203DA2324f9FC67"
81 | },
82 | {
83 | "name": "Ethena USD",
84 | "symbol": "USDe",
85 | "address": "0x28356C7B6087EebaFd1023D292eA9F5327e8F215"
86 | },
87 | {
88 | "name": "Tether USD",
89 | "symbol": "USDT",
90 | "address": "0xd7e9C75C6C05FdE929cAc19bb887892de78819B7"
91 | },
92 | {
93 | "name": "Levered ETH",
94 | "symbol": "levETH",
95 | "address": "0x975f67319f9DA83B403309108d4a8f84031538A6"
96 | },
97 | {
98 | "name": "TAO Token",
99 | "symbol": "TAO",
100 | "address": "0x67025805e2431921C8359A0E1C0c514cFF5fcFDB"
101 | }
102 | ]
103 |
--------------------------------------------------------------------------------
/token_whitelist/sepolia.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Wrapped Ether",
4 | "symbol": "WETH",
5 | "decimals": 18,
6 | "address": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9"
7 | },
8 | {
9 | "name": "USD Coin",
10 | "symbol": "USDC",
11 | "decimals": 6,
12 | "address": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"
13 | },
14 | {
15 | "name": "Tether USD",
16 | "symbol": "USDT",
17 | "decimals": 6,
18 | "address": "0xaa8e23fb1079ea71e0a56f48a2aa51851d8433d0"
19 | },
20 | {
21 | "name": "Dai Stablecoin",
22 | "symbol": "DAI",
23 | "decimals": 18,
24 | "address": "0x3e622317f8c93f7328350cf0b56d9ed4c620c5d6"
25 | },
26 | {
27 | "name": "ChainLink Token",
28 | "symbol": "LINK",
29 | "decimals": 18,
30 | "address": "0x779877a7b0d9e8603169ddbd7836e478b4624789"
31 | },
32 | {
33 | "name": "Uniswap",
34 | "symbol": "UNI",
35 | "decimals": 18,
36 | "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"
37 | }
38 | ]
--------------------------------------------------------------------------------
/version/version.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | var (
4 | // Version can also be set through tag release at build time
5 | semver = "1.5.0"
6 | revision = "unknow"
7 | )
8 |
9 | // Get return the version. Note that we're injecting this at build time when we tag release
10 | func Get() string {
11 | return semver
12 | }
13 |
14 | func Commit() string {
15 | return revision
16 | }
17 |
--------------------------------------------------------------------------------