├── .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 | 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 | --------------------------------------------------------------------------------