├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── actions │ └── test-coverage │ │ ├── action.yaml │ │ └── coverage.sh ├── dependabot.yml └── workflows │ ├── bindings.yml │ ├── changelog.yml │ ├── check-anvil-state.yml │ ├── check-fmt.yml │ ├── check-mocks.yml │ ├── contracts.yml │ ├── golangci-lint.yml │ ├── test-coverage.yml │ └── unit-tests.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── M2-contracts ├── .gitignore ├── abigen-with-interfaces.Dockerfile ├── anvil │ ├── README.md │ ├── contracts-deployed-anvil-state.json │ ├── deploy-contracts-save-anvil-state.sh │ ├── start-anvil-chain-with-el-and-avs-deployed.sh │ └── utils.sh ├── bindings │ ├── BLSApkRegistry │ │ └── binding.go │ ├── ContractsRegistry │ │ └── binding.go │ ├── DelegationManager │ │ └── binding.go │ ├── EigenPod │ │ └── binding.go │ ├── EigenPodManager │ │ └── binding.go │ ├── IAVSDirectory │ │ └── binding.go │ ├── IBLSSignatureChecker │ │ └── binding.go │ ├── IERC20 │ │ └── binding.go │ ├── IRewardsCoordinator │ │ └── binding.go │ ├── ISlasher │ │ └── binding.go │ ├── IStrategy │ │ └── binding.go │ ├── IndexRegistry │ │ └── binding.go │ ├── MockAvsServiceManager │ │ └── binding.go │ ├── OperatorStateRetriever │ │ └── binding.go │ ├── RegistryCoordinator │ │ └── binding.go │ ├── ServiceManagerBase │ │ └── binding.go │ ├── StakeRegistry │ │ └── binding.go │ └── StrategyManager │ │ └── binding.go ├── foundry.toml ├── generate-bindings.sh ├── script │ ├── DeployMockAvs.s.sol │ ├── DeployMockAvsRegistries.s.sol │ ├── DeployTokensStrategiesCreateQuorums.s.sol │ ├── RegisterOperatorsWithEigenlayer.s.sol │ ├── input │ │ └── 31337 │ │ │ └── ops_addresses.json │ ├── output │ │ └── 31337 │ │ │ ├── eigenlayer_deployment_output.json │ │ │ ├── mockAvs_deployment_output.json │ │ │ └── token_and_strategy_deployment_output.json │ └── parsers │ │ ├── ConfigsReadWriter.sol │ │ ├── EigenlayerContractsParser.sol │ │ ├── MockAvsContractsParser.sol │ │ └── TokensAndStrategiesContractsParser.sol └── src │ ├── ContractsRegistry.sol │ ├── MockAvsServiceManager.sol │ └── MockERC20.sol ├── Makefile ├── README.md ├── aws ├── config.go ├── kms │ ├── client.go │ ├── get_public_key.go │ ├── get_public_key_test.go │ └── get_signature.go └── secretmanager │ ├── secretmanager.go │ └── secretmanager_test.go ├── chainio ├── README.md ├── clients │ ├── avsregistry │ │ ├── bindings.go │ │ ├── builder.go │ │ ├── reader.go │ │ ├── reader_test.go │ │ ├── subscriber.go │ │ ├── subscriber_test.go │ │ ├── writer.go │ │ └── writer_test.go │ ├── builder.go │ ├── clients_test.go │ ├── eigenpod │ │ ├── bindings.go │ │ ├── builder.go │ │ ├── reader.go │ │ └── writer.go │ ├── elcontracts │ │ ├── bindings.go │ │ ├── builder.go │ │ ├── reader.go │ │ ├── reader_test.go │ │ ├── types.go │ │ ├── writer.go │ │ └── writer_test.go │ ├── eth │ │ ├── client.go │ │ ├── instrumented_client.go │ │ └── instrumented_client_test.go │ ├── fireblocks │ │ ├── cancel_transaction.go │ │ ├── client.go │ │ ├── client_test.go │ │ ├── contract_call.go │ │ ├── get_asset_addresses.go │ │ ├── get_transaction.go │ │ ├── list_contracts.go │ │ ├── list_external_accounts.go │ │ ├── list_vault_accounts.go │ │ ├── status.go │ │ ├── transaction.go │ │ └── transfer.go │ ├── mocks │ │ ├── fireblocks.go │ │ └── wallet.go │ └── wallet │ │ ├── README.md │ │ ├── fireblocks_wallet.go │ │ ├── fireblocks_wallet_test.go │ │ ├── privatekey_wallet.go │ │ ├── privatekey_wallet_test.go │ │ └── wallet.go ├── gen.go ├── txmgr │ ├── README.md │ ├── geometric │ │ ├── geometric.go │ │ ├── geometric_example_test.go │ │ ├── geometric_test.go │ │ └── metrics.go │ ├── simple-tx-manager-flow.png │ ├── simple.go │ ├── simple_test.go │ └── txmgr.go └── utils │ └── utils.go ├── cmd ├── egnaddrs │ ├── README.md │ ├── main.go │ └── main_test.go └── egnkey │ ├── README.md │ ├── generate │ └── generate.go │ ├── main.go │ ├── main_test.go │ ├── operatorid │ └── operatorid.go │ └── store │ └── store.go ├── contracts ├── .gitignore ├── abigen-with-interfaces.Dockerfile ├── anvil │ ├── README.md │ ├── contracts-deployed-anvil-state.json │ ├── deploy-contracts-save-anvil-state.sh │ ├── start-anvil-chain-with-el-and-avs-deployed.sh │ └── utils.sh ├── bindings │ ├── AVSDirectory │ │ └── binding.go │ ├── AllocationManager │ │ └── binding.go │ ├── BLSApkRegistry │ │ └── binding.go │ ├── ContractsRegistry │ │ └── binding.go │ ├── DelegationManager │ │ └── binding.go │ ├── EigenPod │ │ └── binding.go │ ├── EigenPodManager │ │ └── binding.go │ ├── IBLSSignatureChecker │ │ └── binding.go │ ├── IERC20 │ │ └── binding.go │ ├── IStrategy │ │ └── binding.go │ ├── IndexRegistry │ │ └── binding.go │ ├── MockAvsServiceManager │ │ └── binding.go │ ├── MockERC20 │ │ └── binding.go │ ├── OperatorStateRetriever │ │ └── binding.go │ ├── PermissionController │ │ └── binding.go │ ├── RegistryCoordinator │ │ └── binding.go │ ├── RewardsCoordinator │ │ └── binding.go │ ├── ServiceManagerBase │ │ └── binding.go │ ├── SlashingRegistryCoordinator │ │ └── binding.go │ ├── SocketRegistry │ │ └── binding.go │ ├── StakeRegistry │ │ └── binding.go │ └── StrategyManager │ │ └── binding.go ├── config │ └── core │ │ └── 31337.json ├── foundry.toml ├── generate-bindings.sh ├── script │ ├── DeployMockAvs.s.sol │ ├── DeployMockAvsRegistries.s.sol │ ├── DeployTokensStrategiesCreateQuorums.s.sol │ ├── RegisterOperatorsWithEigenlayer.s.sol │ ├── input │ │ └── 31337 │ │ │ └── ops_addresses.json │ ├── output │ │ └── 31337 │ │ │ ├── eigenlayer_deployment_output.json │ │ │ ├── mockAvs_deployment_output.json │ │ │ └── token_and_strategy_deployment_output.json │ └── parsers │ │ ├── ConfigsReadWriter.sol │ │ ├── EigenlayerContractsParser.sol │ │ ├── MockAvsContractsParser.sol │ │ └── TokensAndStrategiesContractsParser.sol ├── src │ ├── ContractsRegistry.sol │ ├── MockAvsServiceManager.sol │ └── MockERC20.sol └── test │ └── RegistrationEncoding.t.sol ├── crypto ├── bls │ ├── attestation.go │ └── attestation_test.go ├── bn254 │ └── utils.go ├── ecdsa │ ├── utils.go │ └── utils_test.go └── utils │ ├── batch_key.go │ ├── batch_key_test.go │ ├── test_data │ ├── bls_keys │ │ ├── keys │ │ │ ├── 1.bls.key.json │ │ │ ├── 2.bls.key.json │ │ │ └── 3.bls.key.json │ │ ├── password.txt │ │ └── private_key_hex.txt │ └── ecdsa_keys │ │ ├── keys │ │ ├── 1.ecdsa.key.json │ │ ├── 2.ecdsa.key.json │ │ └── 3.ecdsa.key.json │ │ ├── password.txt │ │ └── private_key_hex.txt │ └── types.go ├── go.mod ├── go.sum ├── integration-tests └── rewards_scripts_test.go ├── internal └── fakes │ ├── avs_registry.go │ └── eth_client.go ├── logging ├── README.md ├── constants.go ├── logger.go ├── slog_logger.go └── zap_logger.go ├── metrics ├── README.md ├── collectors │ ├── economic │ │ ├── economic.go │ │ └── economic_test.go │ └── rpc_calls │ │ ├── rpc_calls.go │ │ └── rpc_calls_test.go ├── eigenmetrics.go ├── eigenmetrics_example_test.go ├── eigenmetrics_test.go ├── metrics.go └── noop_metrics.go ├── nodeapi ├── README.md ├── nodeapi.go ├── nodeapi_example_test.go └── nodeapi_test.go ├── services ├── README.md ├── avsregistry │ ├── avsregistry.go │ ├── avsregistry_chaincaller.go │ ├── avsregistry_chaincaller_test.go │ └── avsregistry_fake.go ├── bls_aggregation │ ├── blsagg.go │ └── blsagg_test.go └── operatorsinfo │ ├── operatorsinfo.go │ ├── operatorsinfo_inmemory.go │ ├── operatorsinfo_inmemory_test.go │ ├── operatorsinfo_subgraph.go │ └── operatorsinfo_subgraph_test.go ├── signer ├── bls │ ├── cerberus │ │ ├── cerberus.go │ │ └── cerberus_test.go │ ├── local │ │ ├── local.go │ │ ├── local_test.go │ │ └── testdata │ │ │ └── test.bls.json │ ├── privatekey │ │ ├── privatekey.go │ │ └── privatekey_test.go │ ├── signer.go │ └── types │ │ ├── errors.go │ │ └── types.go ├── go.mod └── go.sum ├── signerv2 ├── README.md ├── config.go ├── kms_signer.go ├── kms_signer_test.go ├── mockdata │ └── dummy.key.json ├── signer.go ├── signer_test.go └── web3_signer.go ├── testutils ├── anvil.go ├── crypto.go ├── localstack.go ├── logger.go ├── logging.go ├── test_data.go └── testclients │ └── testclients.go ├── types ├── avs.go ├── basic.go ├── constants.go ├── errors.go ├── operator.go ├── operator_metadata.go ├── operator_metadata_test.go ├── operator_test.go └── test.go └── utils ├── errors.go ├── utils.go └── utils_test.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | 14 | **To Reproduce** 15 | 16 | 17 | **Expected behavior** 18 | 19 | 20 | **Screenshots** 21 | 22 | 23 | **OS details** 24 | 28 | 29 | **Additional context** 30 | 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | **Describe alternatives you've considered** 17 | 18 | 19 | **Additional context** 20 | 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # . 2 | 3 | ### What Changed? 4 | 5 | 6 | ### Reviewer Checklist 7 | - [ ] Code is well-documented 8 | - [ ] Code adheres to Go [naming conventions](https://go.dev/doc/effective_go#names) 9 | - [ ] Code deprecates any old functionality before removing it -------------------------------------------------------------------------------- /.github/actions/test-coverage/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'Go coverage report' 2 | description: 'This action updates adds an HTML coverage report and SVG badge to your wiki' 3 | branding: 4 | color: blue 5 | icon: award 6 | 7 | inputs: 8 | report: 9 | description: Generate an HTML coverage report. 10 | default: true 11 | chart: 12 | description: Generate a coverage over time chart. 13 | default: false 14 | amend: 15 | description: Amend wiki, avoiding spurious commits. 16 | default: false 17 | go-version: 18 | description: The Go version to download (if necessary) and use. 19 | default: '1.21' 20 | 21 | runs: 22 | using: "composite" 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v4 26 | 27 | - name: Checkout wiki 28 | uses: actions/checkout@v4 29 | with: 30 | repository: ${{github.repository}}.wiki 31 | token: ${{ github.token }} 32 | path: ./.github/wiki/ 33 | 34 | - name: Set up Go 35 | uses: actions/setup-go@v5 36 | with: 37 | go-version: ${{inputs.go-version}} 38 | 39 | - name: Generate coverage report 40 | shell: bash 41 | env: 42 | INPUT_CHART: ${{inputs.chart}} 43 | INPUT_REPORT: ${{inputs.report}} 44 | run: | 45 | ${{github.action_path}}/coverage.sh ./.github/wiki/ 46 | 47 | - name: Push to wiki 48 | shell: bash 49 | run: | 50 | cd ./.github/wiki/ 51 | git add --all 52 | git diff-index --quiet HEAD && exit 53 | git config --local user.name "GitHub Action" 54 | git config --local user.email "action@github.com" 55 | git remote set-url --push origin https://${{ github.token }}@github.com/Layr-Labs/eigensdk-go.wiki.git 56 | test ${{inputs.amend}} == "true" && \ 57 | git commit --amend --no-edit && git push --force-with-lease || \ 58 | git commit -m "Update coverage" && git push https://${{ github.token }}@github.com/Layr-Labs/eigensdk-go.wiki.git 59 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/bindings.yml: -------------------------------------------------------------------------------- 1 | name: Bindings 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | pull_request: 8 | merge_group: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | generate_bindings: 15 | name: Generate bindings 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | submodules: recursive 22 | 23 | - name: Install go1.21 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version: "1.21" 27 | 28 | - name: Add Ethereum PPA 29 | run: sudo add-apt-repository -y ppa:ethereum/ethereum 30 | 31 | - name: Install Abigen 32 | run: sudo apt-get update && sudo apt-get install ethereum 33 | 34 | - name: Show abigen version 35 | run: abigen --version 36 | 37 | - name: Install Foundry 38 | uses: foundry-rs/foundry-toolchain@v1 39 | with: 40 | version: v0.3.0 41 | 42 | - name: Run make bindings 43 | run: make bindings 44 | 45 | check_bindings: 46 | name: Check bindings are up-to-date 47 | runs-on: ubuntu-latest 48 | 49 | steps: 50 | - uses: actions/checkout@v4 51 | with: 52 | submodules: recursive 53 | 54 | # This step is needed to know if the contracts were changed. 55 | - uses: dorny/paths-filter@v3 56 | id: filter 57 | with: 58 | filters: | 59 | contracts: 60 | - 'contracts/lib/**' 61 | - 'contracts/src/**' 62 | bindings: 63 | - 'contracts/bindings/**' 64 | 65 | 66 | # This step runs only if some contract changed. 67 | # It checks whether the bindings directory have changed. 68 | # If there are no changes, then the bindings are outdated 69 | # and therefore this step will fail. 70 | - name: Check the bindings are up-to-date 71 | if: steps.filter.outputs.contracts == 'true' 72 | run: | 73 | BINDINGS_UPDATED=${{ steps.filter.outputs.bindings }} 74 | if [[ "$BINDINGS_UPDATED" == "false" ]]; then 75 | echo "The bindings are outdated"; 76 | exit 1 77 | fi 78 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Workflow" 2 | on: 3 | merge_group: 4 | pull_request: 5 | types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] 6 | 7 | env: 8 | PR_URL: https://github.com/Layr-Labs/eigensdk-go/pull/${{ github.event.number }} 9 | 10 | jobs: 11 | # Enforces the update of a changelog file on every pull request 12 | # The update in the changelog can be skipped if the pull request 13 | # includes the `changelog-ignore` label. 14 | changelog: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Check CHANGELOG.md is updated 22 | uses: dangoslen/changelog-enforcer@v3 23 | with: 24 | skipLabels: changelog-ignore 25 | 26 | - name: Check the PR URL is included in CHANGELOG.md 27 | if: contains(github.event.pull_request.labels.*.name, 'changelog-ignore') == false 28 | run: git diff CHANGELOG.md | grep $PR_URL CHANGELOG.md 29 | -------------------------------------------------------------------------------- /.github/workflows/check-anvil-state.yml: -------------------------------------------------------------------------------- 1 | name: Check anvil state 2 | on: 3 | push: 4 | branches: 5 | - dev 6 | pull_request: 7 | merge_group: 8 | 9 | jobs: 10 | generate-anvil-state: 11 | name: generate anvil state 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | submodules: recursive 17 | 18 | - name: Install Foundry 19 | uses: foundry-rs/foundry-toolchain@v1 20 | with: 21 | version: stable 22 | 23 | - name: Generate anvil state 24 | run: make deploy-contracts-to-anvil-and-save-state 25 | 26 | check-anvil-state: 27 | name: Check anvil dump is up-to-date 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 0 34 | 35 | # This step is needed to know if the contracts were changed. 36 | - uses: dorny/paths-filter@v3 37 | id: filter 38 | with: 39 | filters: | 40 | contracts: 41 | - 'contracts/lib/**' 42 | - 'contracts/script/**' 43 | - 'contracts/src/**' 44 | anvil_state: 45 | - 'contracts/anvil/contracts-deployed-anvil-state.json' 46 | 47 | # This step runs only if some contract changed. 48 | # It checks whether the anvil state has changed. 49 | # If there are no changes, then the anvil state is outdated 50 | # and therefore this step will fail. 51 | - name: Check the anvil state is up-to-date 52 | if: steps.filter.outputs.contracts == 'true' 53 | run: | 54 | ANVIL_STATE_UPDATED=${{ steps.filter.outputs.anvil_state }} 55 | if [[ "$ANVIL_STATE_UPDATED" == "false" ]]; then 56 | echo "The anvil state is outdated"; 57 | exit 1 58 | fi 59 | -------------------------------------------------------------------------------- /.github/workflows/check-fmt.yml: -------------------------------------------------------------------------------- 1 | name: check that make fmt was run 2 | on: 3 | push: 4 | branches: 5 | - dev 6 | pull_request: 7 | merge_group: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | check-make-fmt: 14 | name: check make fmt 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: install go1.21 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: "1.21" 23 | 24 | - name: Run make fmt 25 | run: make fmt 26 | 27 | - name: Check if make fmt generated changes that should be committed 28 | run: | 29 | if [ -n "$(git status --porcelain)" ]; then 30 | echo "Error: make fmt generated changes that should be committed. Please run 'make fmt' and commit the changes." 31 | git diff 32 | git status 33 | exit 1 34 | fi 35 | -------------------------------------------------------------------------------- /.github/workflows/check-mocks.yml: -------------------------------------------------------------------------------- 1 | name: check mocks up-to-date 2 | on: 3 | push: 4 | branches: 5 | - dev 6 | pull_request: 7 | merge_group: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | make-mocks: 14 | name: make mocks and check for diffs 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: checkout repo 18 | uses: actions/checkout@v4 19 | 20 | - name: install go1.21 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: "1.21" 24 | 25 | - name: run make mocks and check for diffs 26 | run: | 27 | make mocks 28 | if [ ! -z "$(git status --porcelain)" ]; then 29 | printf "Current generated mocks not up to date\n" 30 | git diff 31 | git status 32 | exit 1 33 | fi 34 | -------------------------------------------------------------------------------- /.github/workflows/contracts.yml: -------------------------------------------------------------------------------- 1 | name: Contracts CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [ '**' ] 8 | 9 | env: 10 | FOUNDRY_PROFILE: ci 11 | 12 | jobs: 13 | check: 14 | strategy: 15 | fail-fast: true 16 | 17 | name: Foundry project 18 | runs-on: ubuntu-latest 19 | defaults: 20 | run: 21 | working-directory: ./contracts 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | submodules: recursive 27 | 28 | - name: Install Foundry 29 | uses: foundry-rs/foundry-toolchain@v1 30 | with: 31 | version: stable 32 | 33 | - name: Show Forge version 34 | run: forge --version 35 | 36 | - name: Run Forge fmt 37 | run: forge fmt --check 38 | 39 | - name: Run Forge build 40 | run: forge build 41 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | pull_request: 8 | merge_group: 9 | 10 | jobs: 11 | Lint: 12 | name: Lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: install go 1.21 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: "1.21" 21 | 22 | - name: golangci-lint 23 | uses: golangci/golangci-lint-action@v6 24 | with: 25 | version: latest 26 | args: --timeout 3m 27 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yml: -------------------------------------------------------------------------------- 1 | name: test-coverage 2 | 3 | on: 4 | push: 5 | branches: [ dev ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Set up Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: '1.21' 18 | 19 | - name: Update coverage badge 20 | uses: ./.github/actions/test-coverage 21 | with: 22 | chart: true 23 | amend: true 24 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: unit-tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | pull_request: 8 | merge_group: 9 | 10 | jobs: 11 | Test: 12 | name: Test 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-go@v5 17 | with: 18 | go-version: '1.21' 19 | - name: Test 20 | run: make tests 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE related 2 | .idea/ 3 | 4 | # Tests 5 | coverage.html 6 | coverage.out 7 | 8 | # Misc 9 | **/.DS_Store 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "contracts/lib/eigenlayer-middleware"] 2 | path = contracts/lib/eigenlayer-middleware 3 | url = git@github.com:Layr-Labs/eigenlayer-middleware.git 4 | [submodule "contracts/lib/forge-std"] 5 | path = contracts/lib/forge-std 6 | url = git@github.com:foundry-rs/forge-std 7 | [submodule "M2-contracts/lib/eigenlayer-middleware"] 8 | path = M2-contracts/lib/eigenlayer-middleware 9 | url = git@github.com:Layr-Labs/eigenlayer-middleware.git 10 | [submodule "M2-contracts/lib/forge-std"] 11 | path = M2-contracts/lib/forge-std 12 | url = git@github.com:foundry-rs/forge-std 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to eigensdk-go 2 | 3 | 4 | ### Code of Conduct 5 | 6 | Have you read the [code of conduct](https://github.com/Layr-Labs/eigensdk-go/blob/main/CODE_OF_CONDUCT.md)? 7 | 8 | ## Bugs and Feature Request 9 | 10 | Before you make your changes, check to see if an [issue](https://github.com/Layr-Labs/eigensdk-go/issues) exists already for the change you want to make. 11 | 12 | ### Don't see your issue? Open one 13 | 14 | If you spot something new, open an issue using a [template](https://github.com/Layr-Labs/eigensdk-go/issues/new/choose). We'll use the issue to have a conversation about the problem you want to fix. 15 | 16 | ### Open a Pull Request 17 | Before making a pull request, please fork the repository and create a new branch. 18 | 19 | When you're done making changes and you'd like to propose them for review, use the pull request template to open your PR (pull request). 20 | 21 | If your PR is not ready for review and merge because you are still working on it, please convert it to draft and add to it the label `wip` (work in progress). This label allows to filter correctly the rest of PR not `wip`. 22 | 23 | ### Do you intend to add a new feature or change an existing one? 24 | 25 | Suggest your change by opening an issue and starting a discussion. 26 | 27 | ### Improving Issues and PR 28 | 29 | Please add, if possible, a reviewer, assignees and labels to your issue and PR. 30 | 31 | ## DOs and DON'Ts 32 | 33 | Please do: 34 | 35 | * **DO** give priority to the current style of the project or file you're changing even if it diverges from the general guidelines. 36 | * **DO** include tests when adding new features. When fixing bugs, start with adding a test that highlights how the current behavior is broken. 37 | * **DO** especially follow our rules in the [Contributing](https://github.com/Layr-Labs/eigensdk-go/blob/master/CODE_OF_CONDUCT.md#contributing) section of our code of conduct. 38 | * **DO** write idiomatic golang code 39 | 40 | Please do not: 41 | 42 | * **DON'T** fill the issues and PR descriptions vaguely. The elements in the templates are there for a good reason. Help the team. 43 | * **DON'T** surprise us with big pull requests. Instead, file an issue and start a discussion so we can agree on a direction before you invest a large amount of time. 44 | 45 | ## Branch Naming 46 | 47 | Branch names must follow `kebab-case` pattern. Follow the pattern `feature/` or `fix/` `(folder/)` when it is possible and add issue reference if applicable. 48 | 49 | ## Commit Naming 50 | 51 | Commits must follow the `(): ` pattern, as stated in the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) 52 | 53 | ## Security Bugs 54 | Please report security vulnerabilities to security@eigenlabs.org. Do NOT report security bugs via Github Issues. -------------------------------------------------------------------------------- /M2-contracts/.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 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /M2-contracts/abigen-with-interfaces.Dockerfile: -------------------------------------------------------------------------------- 1 | # use the below command to build this docker image locally 2 | # docker build -t abigen-with-interfaces -f abigen-with-interfaces.Dockerfile . 3 | FROM golang:1.21 as build 4 | 5 | # this installs our fork of abigen with also creates interfaces for the binding structs 6 | # See https://github.com/ethereum/go-ethereum/pull/28279 7 | RUN apt update && \ 8 | apt install git -y && \ 9 | git clone https://github.com/samlaf/go-ethereum.git && \ 10 | cd go-ethereum/cmd/abigen && \ 11 | git checkout issue-28278/abigen-interfaces && \ 12 | go build . 13 | 14 | 15 | FROM debian:latest 16 | COPY --from=build /go/go-ethereum/cmd/abigen/abigen /usr/local/bin/abigen 17 | ENTRYPOINT [ "abigen"] 18 | -------------------------------------------------------------------------------- /M2-contracts/anvil/README.md: -------------------------------------------------------------------------------- 1 | # Integration Tests 2 | 3 | We store an anvil state files in this directory, so that we can start an anvil chain with the correct state for integration tests. 4 | ``` 5 | anvil --load-state STATE_FILE.json 6 | ``` 7 | 8 | ## Eigenlayer deployment state file 9 | `eigenlayer-deployed-anvil-state.json` contains the eigenlayer deployment. 10 | 11 | It was created by running this [deploy script](https://github.com/Layr-Labs/eigenlayer-contracts/blob/2cb9ed107c6c918b9dfbac94cd71b4ab7c94e8c2/script/testing/M2_Deploy_From_Scratch.s.sol). If you ever need to redeploy a new version of eigenlayer contracts, first start an anvil chain that dumps its state after exiting 12 | ``` 13 | anvil --dump-state eigenlayer-deployed-anvil-state.json 14 | ``` 15 | Then run the deploy script 16 | ``` 17 | forge script script/testing/M2_Deploy_From_Scratch.s.sol --rpc-url http://localhost:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --broadcast --sig "run(string memory configFile)" -- M2_deploy_from_scratch.anvil.config.json 18 | ``` 19 | and finally kill the anvil chain with `Ctrl-C`. Make sure to copy the deployment [output file](https://github.com/Layr-Labs/eigenlayer-contracts/blob/master/script/output/M2_from_scratch_deployment_data.json) to [eigenlayer_deployment_output.json](../../contracts/script/output/31337/eigenlayer_deployment_output.json) so that the tests can find the deployed contracts. 20 | 21 | See the main [README](../../README.md#dependencies) to understand why we deploy from the `experimental-reduce-strategy-manager-bytecode-size` branch of eigenlayer-contracts. -------------------------------------------------------------------------------- /M2-contracts/anvil/start-anvil-chain-with-el-and-avs-deployed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit -o nounset -o pipefail 4 | 5 | # cd to the directory of this script so that this can be run from anywhere 6 | anvil_dir=$( 7 | cd "$(dirname "${BASH_SOURCE[0]}")" 8 | pwd -P 9 | ) 10 | root_dir=$(realpath $anvil_dir/../..) 11 | 12 | set -a 13 | source $anvil_dir/utils.sh 14 | set +a 15 | 16 | # start an anvil instance in the background that has eigenlayer contracts deployed 17 | # we start anvil in the background so that we can run the below script 18 | start_anvil_docker $anvil_dir/contracts-deployed-anvil-state.json "" 19 | 20 | cd $root_dir/contracts 21 | # we need to restart the anvil chain at the correct block, otherwise the indexRegistry has a quorumUpdate at the block number 22 | # at which it was deployed (aka quorum was created/updated), but when we start anvil by loading state file it starts at block number 0 23 | # so calling getOperatorListAtBlockNumber reverts because it thinks there are no quorums registered at block 0 24 | # advancing chain manually like this is a current hack until https://github.com/foundry-rs/foundry/issues/6679 is merged 25 | cast rpc anvil_mine 200 --rpc-url http://localhost:8545 > /dev/null 26 | echo "Anvil is ready. Advanced chain to block-number:" $(cast block-number) 27 | 28 | 29 | docker attach anvil 30 | -------------------------------------------------------------------------------- /M2-contracts/anvil/utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o nounset 4 | 5 | parent_path=$( 6 | cd "$(dirname "${BASH_SOURCE[0]}")" 7 | pwd -P 8 | ) 9 | 10 | FOUNDRY_IMAGE=ghcr.io/foundry-rs/foundry:stable 11 | 12 | clean_up() { 13 | # Check if the exit status is non-zero 14 | exit_status=$? 15 | if [ $exit_status -ne 0 ]; then 16 | echo "Script exited due to set -e on line $1 with command '$2'. Exit status: $exit_status" 17 | fi 18 | } 19 | # Use trap to call the clean_up function when the script exits 20 | trap 'clean_up $LINENO "$BASH_COMMAND"' ERR 21 | 22 | # start_anvil_docker $LOAD_STATE_FILE $DUMP_STATE_FILE 23 | start_anvil_docker() { 24 | LOAD_STATE_FILE=$1 25 | DUMP_STATE_FILE=$2 26 | LOAD_STATE_VOLUME_DOCKER_ARG=$([[ -z $LOAD_STATE_FILE ]] && echo "" || echo "-v $LOAD_STATE_FILE:/load-state.json") 27 | DUMP_STATE_VOLUME_DOCKER_ARG=$([[ -z $DUMP_STATE_FILE ]] && echo "" || echo "-v $DUMP_STATE_FILE:/dump-state.json") 28 | LOAD_STATE_ANVIL_ARG=$([[ -z $LOAD_STATE_FILE ]] && echo "" || echo "--load-state /load-state.json") 29 | DUMP_STATE_ANVIL_ARG=$([[ -z $DUMP_STATE_FILE ]] && echo "" || echo "--dump-state /dump-state.json") 30 | 31 | trap 'docker stop anvil 2>/dev/null || true' EXIT 32 | set -o xtrace 33 | docker run --rm -d --name anvil -p 8545:8545 $LOAD_STATE_VOLUME_DOCKER_ARG $DUMP_STATE_VOLUME_DOCKER_ARG \ 34 | --entrypoint anvil \ 35 | $FOUNDRY_IMAGE \ 36 | $LOAD_STATE_ANVIL_ARG $DUMP_STATE_ANVIL_ARG --host 0.0.0.0 37 | set +o xtrace 38 | sleep 2 39 | } 40 | -------------------------------------------------------------------------------- /M2-contracts/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | solc_version = '0.8.27' 6 | via-ir = true 7 | optimizer = true 8 | optimizer_runs = 200 9 | remappings = [ 10 | "@openzeppelin-upgrades-v4.9.0/=lib/eigenlayer-middleware/lib/eigenlayer-contracts/lib/openzeppelin-contracts-upgradeable-v4.9.0/", 11 | "@openzeppelin-upgrades/=lib/eigenlayer-middleware/lib/openzeppelin-contracts-upgradeable/", 12 | "@openzeppelin-v4.9.0/=lib/eigenlayer-middleware/lib/eigenlayer-contracts/lib/openzeppelin-contracts-v4.9.0/", 13 | "@openzeppelin/=lib/eigenlayer-middleware/lib/openzeppelin-contracts/", 14 | "ds-test/=lib/eigenlayer-middleware/lib/ds-test/src/", 15 | "eigenlayer-contracts/=lib/eigenlayer-middleware/lib/eigenlayer-contracts/", 16 | "eigenlayer-middleware/=lib/eigenlayer-middleware/", 17 | "forge-std/=lib/forge-std/src/", 18 | "openzeppelin-contracts-upgradeable/=lib/eigenlayer-middleware/lib/openzeppelin-contracts-upgradeable/", 19 | "openzeppelin-contracts/=lib/eigenlayer-middleware/lib/openzeppelin-contracts/", 20 | ] 21 | fs_permissions = [{ access = "read-write", path = "./" }] 22 | 23 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 24 | -------------------------------------------------------------------------------- /M2-contracts/generate-bindings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit -o nounset -o pipefail 4 | 5 | # cd to the directory of this script so that this can be run from anywhere 6 | script_path=$( 7 | cd "$(dirname "${BASH_SOURCE[0]}")" 8 | pwd -P 9 | ) 10 | 11 | # build abigen-with-interfaces docker image if it doesn't exist 12 | if [[ "$(docker images -q abigen-with-interfaces 2>/dev/null)" == "" ]]; then 13 | docker build -t abigen-with-interfaces -f abigen-with-interfaces.Dockerfile $script_path 14 | fi 15 | 16 | # TODO: refactor this function.. it got really ugly with the dockerizing of abigen 17 | function create_binding { 18 | contract_dir=$1 19 | contract=$2 20 | binding_dir=$3 21 | echo $contract 22 | mkdir -p $binding_dir/${contract} 23 | contract_json="$contract_dir/out/${contract}.sol/${contract}.json" 24 | solc_abi=$(cat ${contract_json} | jq -r '.abi') 25 | solc_bin=$(cat ${contract_json} | jq -r '.bytecode.object') 26 | 27 | mkdir -p data 28 | echo ${solc_abi} >data/tmp.abi 29 | echo ${solc_bin} >data/tmp.bin 30 | 31 | rm -f $binding_dir/${contract}/binding.go 32 | docker run -v $(realpath $binding_dir):/home/binding_dir -v .:/home/repo abigen-with-interfaces --bin=/home/repo/data/tmp.bin --abi=/home/repo/data/tmp.abi --pkg=contract${contract} --out=/home/binding_dir/${contract}/binding.go 33 | rm -rf data/tmp.abi data/tmp.bin 34 | } 35 | 36 | path=$1 37 | echo "Generating bindings for contracts in path: $path" 38 | cd "$path" 39 | pwd 40 | 41 | contracts=$2 42 | bindings_path=$3 43 | 44 | forge build 45 | echo "Generating bindings for contracts: $contracts" 46 | for contract in $contracts; do 47 | sleep 1 # this is a hack to fix the issue with abigen randomly failing for some contracts 48 | create_binding . "$contract" "$bindings_path" 49 | done -------------------------------------------------------------------------------- /M2-contracts/script/DeployMockAvs.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.9; 3 | 4 | import "./DeployMockAvsRegistries.s.sol"; 5 | import "forge-std/console.sol"; 6 | 7 | // forge script script/DeployMockAvs.s.sol --rpc-url $RPC_URL --private-key $PRIVATE_KEY --etherscan-api-key $ETHERSCAN_API_KEY --broadcast --verify 8 | contract DeployMockAvs is DeployMockAvsRegistries { 9 | MockAvsServiceManager public mockAvsServiceManager; 10 | MockAvsServiceManager public mockAvsServiceManagerImplementation; 11 | 12 | function run() public virtual { 13 | // The ContractsRegistry contract should always be deployed at this address on anvil 14 | // it's the address of the contract created at nonce 0 by the first anvil account (0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) 15 | ContractsRegistry contractsRegistry = ContractsRegistry( 16 | 0x5FbDB2315678afecb367f032d93F642f64180aa3 17 | ); 18 | EigenlayerContracts 19 | memory eigenlayerContracts = _loadEigenlayerDeployedContracts(); 20 | MockAvsOpsAddresses memory addressConfig = _loadAvsOpsAddresses( 21 | "ops_addresses" 22 | ); 23 | 24 | vm.startBroadcast(); 25 | 26 | // Deploy proxy admin for ability to upgrade proxy contracts 27 | // Note: can't deploy ProxyAdmin in setUp function, b/c its owner is not set correctly if so. 28 | // not sure why... 29 | emptyContract = new EmptyContract(); 30 | mockAvsProxyAdmin = new ProxyAdmin(); 31 | mockAvsServiceManager = MockAvsServiceManager( 32 | address( 33 | new TransparentUpgradeableProxy( 34 | address(emptyContract), 35 | address(mockAvsProxyAdmin), 36 | "" 37 | ) 38 | ) 39 | ); 40 | MockAvsContracts 41 | memory mockAvsContracts = _deploymockAvsRegistryContracts( 42 | eigenlayerContracts, 43 | addressConfig, 44 | mockAvsServiceManager, 45 | mockAvsServiceManagerImplementation 46 | ); 47 | mockAvsServiceManagerImplementation = new MockAvsServiceManager( 48 | registryCoordinator, 49 | eigenlayerContracts.avsDirectory, 50 | eigenlayerContracts.rewardsCoordinator 51 | ); 52 | 53 | mockAvsProxyAdmin.upgradeAndCall( 54 | TransparentUpgradeableProxy( 55 | payable(address(mockAvsServiceManager)) 56 | ), 57 | address(mockAvsServiceManagerImplementation), 58 | abi.encodeWithSelector( 59 | mockAvsServiceManager.initialize.selector, 60 | addressConfig.communityMultisig 61 | ) 62 | ); 63 | require( 64 | Ownable(address(mockAvsServiceManager)).owner() != address(0), 65 | "Owner uninitialized" 66 | ); 67 | 68 | if (block.chainid == 31337 || block.chainid == 1337) { 69 | _writeContractsToRegistry( 70 | contractsRegistry, 71 | eigenlayerContracts, 72 | mockAvsContracts 73 | ); 74 | } 75 | vm.stopBroadcast(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /M2-contracts/script/input/31337/ops_addresses.json: -------------------------------------------------------------------------------- 1 | { 2 | "communityMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 3 | "pauser": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 4 | "churner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 5 | "ejector": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 6 | } -------------------------------------------------------------------------------- /M2-contracts/script/output/31337/eigenlayer_deployment_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "addresses": { 3 | "avsDirectory": "0x0165878A594ca255338adfa4d48449f69242Eb8F", 4 | "avsDirectoryImplementation": "0x9A676e781A523b5d0C0e43731313A708CB607508", 5 | "baseStrategyImplementation": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", 6 | "delegationManager": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", 7 | "delegationManagerImplementation": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", 8 | "eigenLayerPauserReg": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", 9 | "eigenLayerProxyAdmin": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", 10 | "eigenPodBeacon": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e", 11 | "eigenPodImplementation": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", 12 | "eigenPodManager": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", 13 | "eigenPodManagerImplementation": "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1", 14 | "emptyContract": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", 15 | "numStrategiesDeployed": 0, 16 | "rewardsCoordinator": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", 17 | "rewardsCoordinatorImplementation": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE", 18 | "slasher": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", 19 | "slasherImplementation": "0x0B306BF915C4d645ff596e518fAf3F9669b97016", 20 | "strategies": { 21 | "WETH": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" 22 | }, 23 | "strategyManager": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", 24 | "strategyManagerImplementation": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82", 25 | "token": { 26 | "tokenProxyAdmin": "0x0000000000000000000000000000000000000000", 27 | "EIGEN": "0x0000000000000000000000000000000000000000", 28 | "bEIGEN": "0x0000000000000000000000000000000000000000", 29 | "EIGENImpl": "0x0000000000000000000000000000000000000000", 30 | "bEIGENImpl": "0x0000000000000000000000000000000000000000", 31 | "eigenStrategy": "0x0000000000000000000000000000000000000000", 32 | "eigenStrategyImpl": "0x0000000000000000000000000000000000000000" 33 | } 34 | }, 35 | "chainInfo": { 36 | "chainId": 31337, 37 | "deploymentBlock": 1 38 | }, 39 | "parameters": { 40 | "communityMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 41 | "executorMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 42 | "operationsMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 43 | "pauserMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 44 | "timelock": "0x0000000000000000000000000000000000000000" 45 | } 46 | } -------------------------------------------------------------------------------- /M2-contracts/script/output/31337/mockAvs_deployment_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "addresses": { 3 | "mockAvsServiceManager": "0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690", 4 | "mockAvsServiceManagerImplementation": "0x0000000000000000000000000000000000000000", 5 | "operatorStateRetriever": "0x95401dc811bb5740090279Ba06cfA8fcF6113778", 6 | "proxyAdmin": "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E", 7 | "registryCoordinator": "0x9E545E3C0baAB3E08CdfD552C960A1050f373042", 8 | "registryCoordinatorImplementation": "0x36C02dA8a0983159322a80FFE9F24b1acfF8B570" 9 | } 10 | } -------------------------------------------------------------------------------- /M2-contracts/script/output/31337/token_and_strategy_deployment_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "addresses": { 3 | "erc20mock": "0x7bc06c482DEAd17c0e297aFbC32f6e63d3846650", 4 | "erc20mockstrategy": "0xFD471836031dc5108809D173A067e8486B9047A3" 5 | } 6 | } -------------------------------------------------------------------------------- /M2-contracts/script/parsers/ConfigsReadWriter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.12; 3 | 4 | import "eigenlayer-middleware/src/interfaces/IRegistryCoordinator.sol"; 5 | import "eigenlayer-contracts/src/contracts/strategies/StrategyBase.sol"; 6 | 7 | import "forge-std/Script.sol"; 8 | import "forge-std/StdJson.sol"; 9 | 10 | contract ConfigsReadWriter is Script { 11 | // Forge scripts best practice: https://book.getfoundry.sh/tutorials/best-practices#scripts 12 | // inputFileName should not contain the .json extension, we add it automatically 13 | function readInput( 14 | string memory inputFileName 15 | ) internal view returns (string memory) { 16 | string memory inputDir = string.concat( 17 | vm.projectRoot(), 18 | "/script/input/" 19 | ); 20 | string memory chainDir = string.concat(vm.toString(block.chainid), "/"); 21 | string memory file = string.concat(inputFileName, ".json"); 22 | return vm.readFile(string.concat(inputDir, chainDir, file)); 23 | } 24 | 25 | function readOutput( 26 | string memory outputFileName 27 | ) internal view returns (string memory) { 28 | string memory inputDir = string.concat( 29 | vm.projectRoot(), 30 | "/script/output/" 31 | ); 32 | string memory chainDir = string.concat(vm.toString(block.chainid), "/"); 33 | string memory file = string.concat(outputFileName, ".json"); 34 | return vm.readFile(string.concat(inputDir, chainDir, file)); 35 | } 36 | 37 | function writeOutput( 38 | string memory outputJson, 39 | string memory outputFileName 40 | ) internal { 41 | string memory outputDir = string.concat( 42 | vm.projectRoot(), 43 | "/script/output/" 44 | ); 45 | string memory chainDir = string.concat(vm.toString(block.chainid), "/"); 46 | string memory outputFilePath = string.concat( 47 | outputDir, 48 | chainDir, 49 | outputFileName, 50 | ".json" 51 | ); 52 | vm.writeJson(outputJson, outputFilePath); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /M2-contracts/script/parsers/MockAvsContractsParser.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.9; 3 | 4 | import {RegistryCoordinator} from "eigenlayer-middleware/src/RegistryCoordinator.sol"; 5 | import {OperatorStateRetriever} from "eigenlayer-middleware/src/OperatorStateRetriever.sol"; 6 | 7 | import "../../src/MockAvsServiceManager.sol"; 8 | 9 | import {ConfigsReadWriter} from "./ConfigsReadWriter.sol"; 10 | import "forge-std/StdJson.sol"; 11 | 12 | struct MockAvsContracts { 13 | MockAvsServiceManager mockAvsServiceManager; 14 | RegistryCoordinator registryCoordinator; 15 | OperatorStateRetriever operatorStateRetriever; 16 | } 17 | 18 | contract MockAvsContractsParser is ConfigsReadWriter { 19 | function _loadMockAvsDeployedContracts() 20 | internal 21 | view 22 | returns (MockAvsContracts memory) 23 | { 24 | // Eigenlayer contracts 25 | string memory mockAvsDeployedContracts = readOutput( 26 | "mockavs_deployment_output" 27 | ); 28 | MockAvsServiceManager mockAvsServiceManager = MockAvsServiceManager( 29 | stdJson.readAddress( 30 | mockAvsDeployedContracts, 31 | ".addresses.mockAvsServiceManager" 32 | ) 33 | ); 34 | require( 35 | address(mockAvsServiceManager) != address(0), 36 | "MockAvsContractsParser: mockAvsServiceManager address is 0" 37 | ); 38 | RegistryCoordinator registryCoordinator = RegistryCoordinator( 39 | stdJson.readAddress( 40 | mockAvsDeployedContracts, 41 | ".addresses.registryCoordinator" 42 | ) 43 | ); 44 | require( 45 | address(registryCoordinator) != address(0), 46 | "MockAvsContractsParser: registryCoordinator address is 0" 47 | ); 48 | OperatorStateRetriever operatorStateRetriever = OperatorStateRetriever( 49 | stdJson.readAddress( 50 | mockAvsDeployedContracts, 51 | ".addresses.operatorStateRetriever" 52 | ) 53 | ); 54 | require( 55 | address(operatorStateRetriever) != address(0), 56 | "MockAvsContractsParser: operatorStateRetriever address is 0" 57 | ); 58 | 59 | return 60 | MockAvsContracts( 61 | mockAvsServiceManager, 62 | registryCoordinator, 63 | operatorStateRetriever 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /M2-contracts/script/parsers/TokensAndStrategiesContractsParser.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.9; 3 | 4 | import "@openzeppelin/contracts/interfaces/IERC20.sol"; 5 | import {IStrategyManager, IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; 6 | 7 | import {ConfigsReadWriter} from "./ConfigsReadWriter.sol"; 8 | import "forge-std/StdJson.sol"; 9 | 10 | struct TokenAndStrategyContracts { 11 | IERC20 token; 12 | IStrategy strategy; 13 | } 14 | 15 | // TODO: support more than one token/strategy pair (dee deployTokensAndStrategies script) 16 | contract TokenAndStrategyContractsParser is ConfigsReadWriter { 17 | function _loadTokenAndStrategyContracts() 18 | internal 19 | view 20 | returns (TokenAndStrategyContracts memory) 21 | { 22 | // Token and Strategy contracts 23 | string memory tokenAndStrategyConfigFile = readOutput( 24 | "token_and_strategy_deployment_output" 25 | ); 26 | 27 | bytes memory tokensAndStrategiesConfigsRaw = stdJson.parseRaw( 28 | tokenAndStrategyConfigFile, 29 | ".addresses" 30 | ); 31 | TokenAndStrategyContracts memory tokensAndStrategiesContracts = abi 32 | .decode(tokensAndStrategiesConfigsRaw, (TokenAndStrategyContracts)); 33 | 34 | return tokensAndStrategiesContracts; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /M2-contracts/src/ContractsRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.12; 3 | 4 | // ContractsRegistry store the address of all the contracts deployed for eigenlayer and avss 5 | // It is used for testing purpose only, so that we can retrieve the addresses without having to store them in a json file 6 | // This way integration testing against an anvil chain (started with a saved db) is self-contained 7 | // ASSUMPTION: right now we deploy this contract as the first deployment (nonce=0) using the first anvil address 8 | // 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 to make sure it's always at the address 0x5FbDB2315678afecb367f032d93F642f64180aa3 9 | // forge create src/ContractsRegistry.sol:ContractsRegistry --rpc-url $RPC_URL --private-key $PRIVATE_KEY 10 | contract ContractsRegistry { 11 | mapping(string => address) public contracts; 12 | mapping(uint => string) public contractNames; 13 | uint public contractCount; 14 | 15 | function registerContract(string memory name, address _contract) public { 16 | // we treat redeploys as a bug since this is only meant to be used for testing. 17 | // If new contracts need to be deployed just start from a fresh anvil state. 18 | require(contracts[name] == address(0), "contract already registered"); 19 | contracts[name] = _contract; 20 | contractNames[contractCount] = name; 21 | contractCount++; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /M2-contracts/src/MockAvsServiceManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.12; 3 | 4 | import {IAVSDirectory} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; 5 | import {IRewardsCoordinator} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol"; 6 | 7 | import {IRegistryCoordinator} from "eigenlayer-middleware/src/interfaces/IRegistryCoordinator.sol"; 8 | import {IBLSSignatureChecker} from "eigenlayer-middleware/src/interfaces/IBLSSignatureChecker.sol"; 9 | import {ServiceManagerBase} from "eigenlayer-middleware/src/ServiceManagerBase.sol"; 10 | import {BLSSignatureChecker} from "eigenlayer-middleware/src/BLSSignatureChecker.sol"; 11 | 12 | contract MockAvsServiceManager is ServiceManagerBase, BLSSignatureChecker { 13 | constructor( 14 | IRegistryCoordinator _registryCoordinator, 15 | IAVSDirectory _avsDirectory, 16 | IRewardsCoordinator _rewardsCoordinator 17 | ) 18 | ServiceManagerBase( 19 | _avsDirectory, 20 | _rewardsCoordinator, 21 | _registryCoordinator, 22 | _registryCoordinator.stakeRegistry() 23 | ) 24 | BLSSignatureChecker(_registryCoordinator) 25 | {} 26 | 27 | function initialize(address _initialOwner) external initializer { 28 | // TODO: setting _rewardsInitializer to be _initialOwner for now. 29 | __ServiceManagerBase_init(_initialOwner, _initialOwner); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /M2-contracts/src/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.12; 3 | 4 | import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract MockERC20 is ERC20("Mock Token", "MCK") { 7 | function mint(address account, uint256 amount) public { 8 | _mint(account, amount); 9 | } 10 | 11 | /// WARNING: Vulnerable, bypasses allowance check. Do not use in production! 12 | function transferFrom( 13 | address from, 14 | address to, 15 | uint256 amount 16 | ) public virtual override returns (bool) { 17 | super._transfer(from, to, amount); 18 | return true; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Unit Tests](https://github.com/Layr-Labs/eigensdk-go/actions/workflows/unit-tests.yml/badge.svg) 2 | ![Linter](https://github.com/Layr-Labs/eigensdk-go/actions/workflows/golangci-lint.yml/badge.svg) 3 | ![Go Coverage](https://github.com/Layr-Labs/eigensdk-go/wiki/coverage.svg) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/Layr-Labs/eigensdk-go)](https://goreportcard.com/report/github.com/Layr-Labs/eigensdk-go) 5 | 6 | 7 | 8 | 9 | ## EigenSDK 10 | This SDK provides a set of primitive Go modules for developing AVSs on EigenLayer. 11 | 12 | ## Installation 13 | ``` 14 | go get github.com/Layr-Labs/eigensdk-go 15 | ``` 16 | 17 | ## Modules 18 | We support following modules right now. 19 | > **_NOTE:_** All modules are in active development and interfaces might change. 20 | 21 | * [Logging](./logging) 22 | * [ECDSA Signer](./signerv2) 23 | * [BLS Signer](./signer) 24 | * [ChainIO](./chainio) 25 | * [Services](./services) 26 | 27 | ## Development 28 | Clone the repo 29 | ``` 30 | git clone https://github.com/Layr-Labs/eigensdk-go.git 31 | ``` 32 | Initialize git submodules 33 | ``` 34 | git submodule update --init 35 | ``` 36 | 37 | Follow the [contribution guidelines](CONTRIBUTING.md) to contribute to eigensdk-go 38 | 39 | ## Branches 40 | 41 | For consistency with [eigenlayer-middleware](https://github.com/Layr-Labs/eigenlayer-middleware) and [eigenlayer-contracts](https://github.com/Layr-Labs/eigenlayer-contracts) repos, we no longer use the `master` branch and instead use `dev` as the default branch, which will track as closely as possible the `dev` branch of eigenlayer-middleware (which in turn tracks the `dev` branch of eigenlayer-contracts). This convention will also be followed for other important branches. For eg, the m2-mainnet branch of this repo will track the m2-mainnet branch of eigenlayer-middleware (which tracks the unfortunately named mainnet branch of eigenlayer-contracts), and same with the testnet-holesky branch. 42 | 43 | ## Security Bugs 44 | Please report security vulnerabilities to security@eigenlabs.org. Do NOT report security bugs via Github Issues. 45 | 46 | ## Disclaimer 47 | 🚧 EigenSDK-go is under active development and has not been audited. EigenSDK-go is rapidly being upgraded, features may be added, removed or otherwise improved or modified and interfaces will have breaking changes. EigenSDK-go should be used only for testing purposes and not in production. EigenSDK-go is provided "as is" and Eigen Labs, Inc. does not guarantee its functionality or provide support for its use in production. 🚧 48 | -------------------------------------------------------------------------------- /aws/config.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/aws/aws-sdk-go-v2/config" 8 | "github.com/aws/aws-sdk-go-v2/credentials" 9 | ) 10 | 11 | func GetAWSConfig(accessKey, secretAccessKey, region, endpointURL string) (*aws.Config, error) { 12 | createClient := func(service, region string, options ...interface{}) (aws.Endpoint, error) { 13 | if endpointURL != "" { 14 | return aws.Endpoint{ 15 | PartitionID: "aws", 16 | URL: endpointURL, 17 | SigningRegion: region, 18 | }, nil 19 | } 20 | 21 | // returning EndpointNotFoundError will allow the service to fallback to its default resolution 22 | return aws.Endpoint{}, &aws.EndpointNotFoundError{} 23 | } 24 | customResolver := aws.EndpointResolverWithOptionsFunc(createClient) 25 | 26 | cfg, errCfg := config.LoadDefaultConfig(context.Background(), 27 | config.WithRegion(region), 28 | config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKey, secretAccessKey, "")), 29 | config.WithEndpointResolverWithOptions(customResolver), 30 | config.WithRetryMode(aws.RetryModeStandard), 31 | ) 32 | if errCfg != nil { 33 | return nil, errCfg 34 | } 35 | return &cfg, nil 36 | } 37 | -------------------------------------------------------------------------------- /aws/kms/client.go: -------------------------------------------------------------------------------- 1 | package kms 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Layr-Labs/eigensdk-go/utils" 7 | "github.com/aws/aws-sdk-go-v2/config" 8 | "github.com/aws/aws-sdk-go-v2/service/kms" 9 | ) 10 | 11 | func NewKMSClient(ctx context.Context, region string) (*kms.Client, error) { 12 | config, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) 13 | if err != nil { 14 | return nil, utils.WrapError("failed to load AWS config", err) 15 | } 16 | 17 | c := kms.NewFromConfig(config) 18 | return c, nil 19 | } 20 | -------------------------------------------------------------------------------- /aws/kms/get_public_key.go: -------------------------------------------------------------------------------- 1 | package kms 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "encoding/asn1" 7 | "fmt" 8 | 9 | "github.com/Layr-Labs/eigensdk-go/utils" 10 | "github.com/aws/aws-sdk-go-v2/aws" 11 | "github.com/aws/aws-sdk-go-v2/service/kms" 12 | "github.com/ethereum/go-ethereum/crypto" 13 | ) 14 | 15 | type asn1EcPublicKey struct { 16 | EcPublicKeyInfo asn1EcPublicKeyInfo 17 | PublicKey asn1.BitString 18 | } 19 | 20 | type asn1EcPublicKeyInfo struct { 21 | Algorithm asn1.ObjectIdentifier 22 | Parameters asn1.ObjectIdentifier 23 | } 24 | 25 | // GetECDSAPublicKey retrieves the ECDSA public key for a KMS key 26 | // It assumes the key is set up with `ECC_SECG_P256K1` key spec and `SIGN_VERIFY` key usage 27 | func GetECDSAPublicKey(ctx context.Context, svc *kms.Client, keyId string) (*ecdsa.PublicKey, error) { 28 | getPubKeyOutput, err := svc.GetPublicKey(ctx, &kms.GetPublicKeyInput{ 29 | KeyId: aws.String(keyId), 30 | }) 31 | if err != nil { 32 | text := fmt.Sprintf("failed to get public key for KeyId=%s", keyId) 33 | return nil, utils.WrapError(text, err) 34 | } 35 | 36 | var asn1pubk asn1EcPublicKey 37 | _, err = asn1.Unmarshal(getPubKeyOutput.PublicKey, &asn1pubk) 38 | if err != nil { 39 | text := fmt.Sprintf("failed to unmarshal public key for KeyId=%s", keyId) 40 | return nil, utils.WrapError(text, err) 41 | } 42 | 43 | pubkey, err := crypto.UnmarshalPubkey(asn1pubk.PublicKey.Bytes) 44 | if err != nil { 45 | text := fmt.Sprintf("failed to unmarshal public key for KeyId=%s", keyId) 46 | return nil, utils.WrapError(text, err) 47 | } 48 | 49 | return pubkey, nil 50 | } 51 | -------------------------------------------------------------------------------- /aws/kms/get_public_key_test.go: -------------------------------------------------------------------------------- 1 | package kms_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | eigenkms "github.com/Layr-Labs/eigensdk-go/aws/kms" 10 | "github.com/Layr-Labs/eigensdk-go/testutils" 11 | "github.com/aws/aws-sdk-go-v2/service/kms/types" 12 | "github.com/ethereum/go-ethereum/common" 13 | "github.com/ethereum/go-ethereum/crypto" 14 | "github.com/stretchr/testify/assert" 15 | "github.com/testcontainers/testcontainers-go" 16 | ) 17 | 18 | var ( 19 | mappedLocalstackPort string 20 | keyMetadata *types.KeyMetadata 21 | container testcontainers.Container 22 | ) 23 | 24 | func TestMain(m *testing.M) { 25 | err := setup() 26 | if err != nil { 27 | fmt.Println("Error setting up test environment:", err) 28 | teardown() 29 | os.Exit(1) 30 | } 31 | exitCode := m.Run() 32 | teardown() 33 | os.Exit(exitCode) 34 | } 35 | 36 | func setup() error { 37 | var err error 38 | container, err = testutils.StartLocalstackContainer("get_public_key_test") 39 | if err != nil { 40 | return err 41 | } 42 | mappedPort, err := container.MappedPort(context.Background(), testutils.LocalStackPort) 43 | if err != nil { 44 | return err 45 | } 46 | mappedLocalstackPort = string(mappedPort) 47 | keyMetadata, err = testutils.CreateKMSKey(mappedLocalstackPort) 48 | if err != nil { 49 | return err 50 | } 51 | return nil 52 | } 53 | 54 | func teardown() { 55 | _ = container.Terminate(context.Background()) 56 | } 57 | 58 | func TestGetPublicKey(t *testing.T) { 59 | c, err := testutils.NewKMSClient(mappedLocalstackPort) 60 | assert.Nil(t, err) 61 | assert.NotNil(t, keyMetadata.KeyId) 62 | pk, err := eigenkms.GetECDSAPublicKey(context.Background(), c, *keyMetadata.KeyId) 63 | assert.Nil(t, err) 64 | assert.NotNil(t, pk) 65 | keyAddr := crypto.PubkeyToAddress(*pk) 66 | t.Logf("Public key address: %s", keyAddr.String()) 67 | assert.NotEqual(t, keyAddr, common.Address{0}) 68 | } 69 | -------------------------------------------------------------------------------- /aws/kms/get_signature.go: -------------------------------------------------------------------------------- 1 | package kms 2 | 3 | import ( 4 | "context" 5 | "encoding/asn1" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/service/kms" 9 | "github.com/aws/aws-sdk-go-v2/service/kms/types" 10 | ) 11 | 12 | type asn1EcSig struct { 13 | R asn1.RawValue 14 | S asn1.RawValue 15 | } 16 | 17 | // GetECDSASignature retrieves the ECDSA signature for a message using a KMS key 18 | func GetECDSASignature( 19 | ctx context.Context, svc *kms.Client, keyId string, msg []byte, 20 | ) (r []byte, s []byte, err error) { 21 | signInput := &kms.SignInput{ 22 | KeyId: aws.String(keyId), 23 | SigningAlgorithm: types.SigningAlgorithmSpecEcdsaSha256, 24 | MessageType: types.MessageTypeDigest, 25 | Message: msg, 26 | } 27 | 28 | signOutput, err := svc.Sign(ctx, signInput) 29 | if err != nil { 30 | return nil, nil, err 31 | } 32 | 33 | var sigAsn1 asn1EcSig 34 | _, err = asn1.Unmarshal(signOutput.Signature, &sigAsn1) 35 | if err != nil { 36 | return nil, nil, err 37 | } 38 | 39 | return sigAsn1.R.Bytes, sigAsn1.S.Bytes, nil 40 | } 41 | -------------------------------------------------------------------------------- /aws/secretmanager/secretmanager.go: -------------------------------------------------------------------------------- 1 | package secretmanager 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Layr-Labs/eigensdk-go/utils" 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/config" 9 | "github.com/aws/aws-sdk-go-v2/service/secretsmanager" 10 | ) 11 | 12 | func ReadStringFromSecretManager(ctx context.Context, secretName, region string) (string, error) { 13 | config, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) 14 | if err != nil { 15 | return "", utils.WrapError("error while loading default config", err) 16 | } 17 | 18 | // Create Secrets Manager client 19 | svc := secretsmanager.NewFromConfig(config) 20 | 21 | input := &secretsmanager.GetSecretValueInput{ 22 | SecretId: aws.String(secretName), 23 | VersionStage: aws.String("AWSCURRENT"), // VersionStage defaults to AWSCURRENT if unspecified 24 | } 25 | 26 | result, err := svc.GetSecretValue(ctx, input) 27 | if err != nil { 28 | // For a list of exceptions thrown, see 29 | // https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html 30 | return "", utils.WrapError("error while loading default config", err) 31 | } 32 | 33 | // Decrypts secret using the associated KMS key. 34 | var secretString string = *result.SecretString 35 | 36 | return secretString, nil 37 | } 38 | -------------------------------------------------------------------------------- /aws/secretmanager/secretmanager_test.go: -------------------------------------------------------------------------------- 1 | package secretmanager_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/Layr-Labs/eigensdk-go/aws/secretmanager" 8 | ) 9 | 10 | func TestReadString(t *testing.T) { 11 | t.Skip("skipping test as it's meant for manual runs only") 12 | 13 | secretName := "SECRET_NAME" 14 | region := "REGION" 15 | value, err := secretmanager.ReadStringFromSecretManager(context.Background(), secretName, region) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | t.Logf("Secret value: %s", value) 20 | } 21 | -------------------------------------------------------------------------------- /chainio/README.md: -------------------------------------------------------------------------------- 1 | ## ChainIO 2 | 3 | 4 | ### Interacting with a json-rpc node 5 | 6 | We have a basic [ethClient](./clients/eth/client.go) which simply wraps geth's ethClient and adds some convenience methods. The Client interface is also implemented by [instrumentedClient](./clients/eth/instrumented_client.go) which adds metrics to the ethClient to conform to the node spec's [rpc metrics](https://docs.eigenlayer.xyz/eigenlayer/avs-guides/spec/metrics/metrics-prom-spec#rpc-metrics) requirements. 7 | 8 | 9 | ### Building Transactions 10 | 11 | In order to facilitate reading/writing/subscribing to [eigenlayer core](./clients/elcontracts/) contracts and [avs registry](./clients/avsregistry/) contracts, we use geth's abigen created bindings for low-level interactions, as well as our own high-level clients with higher utility functions: 12 | - [Eigenlayer Contract Bindings](./clients/elcontracts/bindings.go) 13 | - generated by abigen 14 | - low level bindings to eigenlayer core contracts, which wrap our ethClient 15 | - [ELChainReader](./clients/elcontracts/reader.go) / [ELChainWriter](./clients/elcontracts/writer.go) / [ELChainSubscriber](./clients/avsregistry/subscriber.go) 16 | - wraps bindings and adds convenience methods 17 | - These structs should be the only ones used by AVS developers, apart from interacting with an ethClient directly to make direct json rpc calls such as waiting for a transaction receipt. 18 | 19 | There's a similar setup for the [avs registry](./clients/avsregistry/) contracts. 20 | 21 | ### Signing, Sending, and Managing Transactions 22 | 23 | After building transactions, we need to sign them, send them to the network, and manage the nonce and gas price to ensure they are mined. This functionality is provided by: 24 | - [txmgr](./txmgr/README.md) 25 | - uses a wallet to sign and submit transactions, but then manages them by resubmitting with higher gas prices until they are mined. 26 | - [wallet](./clients/wallet) 27 | - uses a signerv2 to sign transactions, sends them to the network and can query for their receipts 28 | - wallet abstraction is needed because "wallets", such as fireblocks, both sign and send transactions to the network (they don't simply return signed bytes so that we can send them ourselves) 29 | - [signerv2](../signerv2/README.md) 30 | - signs transactions -------------------------------------------------------------------------------- /chainio/clients/avsregistry/subscriber.go: -------------------------------------------------------------------------------- 1 | package avsregistry 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 5 | "github.com/ethereum/go-ethereum/event" 6 | 7 | "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth" 8 | blsapkreg "github.com/Layr-Labs/eigensdk-go/contracts/bindings/BLSApkRegistry" 9 | regcoord "github.com/Layr-Labs/eigensdk-go/contracts/bindings/RegistryCoordinator" 10 | "github.com/Layr-Labs/eigensdk-go/logging" 11 | "github.com/Layr-Labs/eigensdk-go/utils" 12 | ) 13 | 14 | type ChainSubscriber struct { 15 | logger logging.Logger 16 | regCoord regcoord.ContractRegistryCoordinatorFilters 17 | blsApkRegistry blsapkreg.ContractBLSApkRegistryFilters 18 | } 19 | 20 | // NewChainSubscriber creates a new instance of ChainSubscriber 21 | // The bindings must be created using websocket ETH Client 22 | func NewChainSubscriber( 23 | regCoord regcoord.ContractRegistryCoordinatorFilters, 24 | blsApkRegistry blsapkreg.ContractBLSApkRegistryFilters, 25 | logger logging.Logger, 26 | ) *ChainSubscriber { 27 | logger = logger.With(logging.ComponentKey, "avsregistry/ChainSubscriber") 28 | 29 | return &ChainSubscriber{ 30 | regCoord: regCoord, 31 | blsApkRegistry: blsApkRegistry, 32 | logger: logger, 33 | } 34 | } 35 | 36 | // NewSubscriberFromConfig creates a new instance of ChainSubscriber 37 | // A websocket ETH Client must be provided 38 | func NewSubscriberFromConfig( 39 | cfg Config, 40 | wsClient eth.WsBackend, 41 | logger logging.Logger, 42 | ) (*ChainSubscriber, error) { 43 | bindings, err := NewBindingsFromConfig(cfg, wsClient, logger) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return NewChainSubscriber(bindings.RegistryCoordinator, bindings.BlsApkRegistry, logger), nil 49 | } 50 | 51 | // Returns a channel that receives new BLS pubkey registration events. 52 | func (s *ChainSubscriber) SubscribeToNewPubkeyRegistrations() (chan *blsapkreg.ContractBLSApkRegistryNewPubkeyRegistration, event.Subscription, error) { 53 | newPubkeyRegistrationChan := make(chan *blsapkreg.ContractBLSApkRegistryNewPubkeyRegistration) 54 | sub, err := s.blsApkRegistry.WatchNewPubkeyRegistration( 55 | &bind.WatchOpts{}, newPubkeyRegistrationChan, nil, 56 | ) 57 | if err != nil { 58 | return nil, nil, utils.WrapError("Failed to subscribe to NewPubkeyRegistration events", err) 59 | } 60 | return newPubkeyRegistrationChan, sub, nil 61 | } 62 | 63 | // Returns a channel that receives operator socket update events. 64 | func (s *ChainSubscriber) SubscribeToOperatorSocketUpdates() (chan *regcoord.ContractRegistryCoordinatorOperatorSocketUpdate, event.Subscription, error) { 65 | operatorSocketUpdateChan := make(chan *regcoord.ContractRegistryCoordinatorOperatorSocketUpdate) 66 | sub, err := s.regCoord.WatchOperatorSocketUpdate( 67 | &bind.WatchOpts{}, operatorSocketUpdateChan, nil, 68 | ) 69 | if err != nil { 70 | return nil, nil, utils.WrapError("Failed to subscribe to OperatorSocketUpdate events", err) 71 | } 72 | return operatorSocketUpdateChan, sub, nil 73 | } 74 | -------------------------------------------------------------------------------- /chainio/clients/clients_test.go: -------------------------------------------------------------------------------- 1 | package clients_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestMain(m *testing.M) { 9 | code := m.Run() 10 | os.Exit(code) 11 | } 12 | -------------------------------------------------------------------------------- /chainio/clients/eigenpod/bindings.go: -------------------------------------------------------------------------------- 1 | package eigenpod 2 | 3 | import ( 4 | ieigenpod "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/IEigenPod" 5 | ieigenpodmanager "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/IEigenPodManager" 6 | "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | ) 10 | 11 | type ContractBindings struct { 12 | Address common.Address 13 | *ieigenpod.IEigenPod 14 | } 15 | 16 | type ContractCallerBindings struct { 17 | Address common.Address 18 | *ieigenpod.IEigenPodCaller 19 | } 20 | 21 | type ManagerContractBindings struct { 22 | Address common.Address 23 | *ieigenpodmanager.IEigenPodManager 24 | } 25 | 26 | type ManagerContractCallerBindings struct { 27 | Address common.Address 28 | *ieigenpodmanager.IEigenPodManagerCaller 29 | } 30 | 31 | func NewContractBindings( 32 | address common.Address, 33 | ethClient eth.HttpBackend, 34 | ) (*ContractBindings, error) { 35 | pod, err := ieigenpod.NewIEigenPod(address, ethClient) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return &ContractBindings{ 40 | Address: address, 41 | IEigenPod: pod, 42 | }, nil 43 | } 44 | 45 | func NewContractCallerBindings( 46 | address common.Address, 47 | ethClient eth.HttpBackend, 48 | ) (*ContractCallerBindings, error) { 49 | pod, err := ieigenpod.NewIEigenPodCaller(address, ethClient) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return &ContractCallerBindings{ 54 | Address: address, 55 | IEigenPodCaller: pod, 56 | }, nil 57 | } 58 | 59 | func NewManagerContractBindings( 60 | address common.Address, 61 | ethClient eth.HttpBackend, 62 | ) (*ManagerContractBindings, error) { 63 | manager, err := ieigenpodmanager.NewIEigenPodManager(address, ethClient) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return &ManagerContractBindings{ 68 | Address: address, 69 | IEigenPodManager: manager, 70 | }, nil 71 | } 72 | 73 | func NewManagerContractCallerBindings( 74 | address common.Address, 75 | ethClient eth.HttpBackend, 76 | ) (*ManagerContractCallerBindings, error) { 77 | manager, err := ieigenpodmanager.NewIEigenPodManagerCaller(address, ethClient) 78 | if err != nil { 79 | return nil, err 80 | } 81 | return &ManagerContractCallerBindings{ 82 | Address: address, 83 | IEigenPodManagerCaller: manager, 84 | }, nil 85 | } 86 | -------------------------------------------------------------------------------- /chainio/clients/eigenpod/builder.go: -------------------------------------------------------------------------------- 1 | package eigenpod 2 | 3 | import ( 4 | "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth" 5 | "github.com/Layr-Labs/eigensdk-go/chainio/txmgr" 6 | "github.com/Layr-Labs/eigensdk-go/logging" 7 | "github.com/Layr-Labs/eigensdk-go/utils" 8 | 9 | "github.com/ethereum/go-ethereum/common" 10 | ) 11 | 12 | func BuildEigenPodClients( 13 | address common.Address, 14 | client eth.HttpBackend, 15 | txMgr txmgr.TxManager, 16 | logger logging.Logger, 17 | ) (*ChainReader, *ChainWriter, *ContractBindings, error) { 18 | eigenPodBindings, err := NewContractBindings(address, client) 19 | if err != nil { 20 | return nil, nil, nil, utils.WrapError("failed to create EigenPod contract bindings", err) 21 | } 22 | 23 | eigenPodChainReader := newChainReader( 24 | &eigenPodBindings.IEigenPodCaller, 25 | client, 26 | logger, 27 | ) 28 | 29 | eigenPodChainWriter := newChainWriter( 30 | eigenPodBindings.IEigenPod, 31 | client, 32 | logger, 33 | txMgr, 34 | ) 35 | 36 | return eigenPodChainReader, eigenPodChainWriter, eigenPodBindings, nil 37 | } 38 | 39 | func BuildEigenPodManagerClients( 40 | address common.Address, 41 | client eth.HttpBackend, 42 | txMgr txmgr.TxManager, 43 | logger logging.Logger, 44 | ) (*ManagerChainReader, *ManagerChainWriter, *ManagerContractBindings, error) { 45 | eigenPodManagerBindings, err := NewManagerContractBindings(address, client) 46 | if err != nil { 47 | return nil, nil, nil, utils.WrapError("failed to create EigenPod manager contract bindings", err) 48 | } 49 | 50 | eigenPodManagerChainReader := newManagerChainReader( 51 | &eigenPodManagerBindings.IEigenPodManagerCaller, 52 | client, 53 | logger, 54 | ) 55 | 56 | eigenPodManagerChainWriter := newManagerChainWriter( 57 | eigenPodManagerBindings.IEigenPodManager, 58 | client, 59 | logger, 60 | txMgr, 61 | ) 62 | 63 | return eigenPodManagerChainReader, eigenPodManagerChainWriter, eigenPodManagerBindings, nil 64 | } 65 | -------------------------------------------------------------------------------- /chainio/clients/eigenpod/reader.go: -------------------------------------------------------------------------------- 1 | package eigenpod 2 | 3 | import ( 4 | ieigenpod "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/IEigenPod" 5 | ieigenpodmanager "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/IEigenPodManager" 6 | "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth" 7 | "github.com/Layr-Labs/eigensdk-go/logging" 8 | "github.com/Layr-Labs/eigensdk-go/utils" 9 | 10 | "github.com/ethereum/go-ethereum/common" 11 | ) 12 | 13 | // ChainReader is a reader for the EigenPod contract. 14 | // We want it to be different from the ManagerChainReader since as a user, 15 | // I only need this reader to manage my EigenPod. So there's no need for ManagerChainReader 16 | type ChainReader struct { 17 | logger logging.Logger 18 | ethClient eth.HttpBackend 19 | *ieigenpod.IEigenPodCaller 20 | } 21 | 22 | // ManagerChainReader is a reader for the EigenPodManager contract. 23 | // We want it to be different from the ChainReader since as a user, this is only 24 | // needed to get overall state of all the EigenPods in the system. 25 | type ManagerChainReader struct { 26 | logger logging.Logger 27 | ethClient eth.HttpBackend 28 | *ieigenpodmanager.IEigenPodManagerCaller 29 | } 30 | 31 | func newChainReader( 32 | eigenPod *ieigenpod.IEigenPodCaller, 33 | ethClient eth.HttpBackend, 34 | logger logging.Logger, 35 | ) *ChainReader { 36 | logger = logger.With(logging.ComponentKey, "eigenpod/reader") 37 | 38 | return &ChainReader{ 39 | logger: logger, 40 | ethClient: ethClient, 41 | IEigenPodCaller: eigenPod, 42 | } 43 | } 44 | 45 | func newManagerChainReader( 46 | manager *ieigenpodmanager.IEigenPodManagerCaller, 47 | ethClient eth.HttpBackend, 48 | logger logging.Logger, 49 | ) *ManagerChainReader { 50 | logger = logger.With(logging.ComponentKey, "eigenpodmanager/reader") 51 | 52 | return &ManagerChainReader{ 53 | logger: logger, 54 | ethClient: ethClient, 55 | IEigenPodManagerCaller: manager, 56 | } 57 | } 58 | 59 | func NewReader( 60 | eigenPodAddress common.Address, 61 | ethClient eth.HttpBackend, 62 | logger logging.Logger, 63 | ) (*ChainReader, error) { 64 | pod, err := NewContractCallerBindings(eigenPodAddress, ethClient) 65 | if err != nil { 66 | return nil, utils.WrapError("Failed to create EigenPod contract", err) 67 | } 68 | 69 | return newChainReader(pod.IEigenPodCaller, ethClient, logger), nil 70 | } 71 | 72 | func NewManagerReader( 73 | eigenPodManagerAddress common.Address, 74 | ethClient eth.HttpBackend, 75 | logger logging.Logger, 76 | ) (*ManagerChainReader, error) { 77 | manager, err := NewManagerContractCallerBindings(eigenPodManagerAddress, ethClient) 78 | if err != nil { 79 | return nil, utils.WrapError("Failed to create EigenPodManager contract", err) 80 | } 81 | 82 | return newManagerChainReader(manager.IEigenPodManagerCaller, ethClient, logger), nil 83 | } 84 | -------------------------------------------------------------------------------- /chainio/clients/eigenpod/writer.go: -------------------------------------------------------------------------------- 1 | package eigenpod 2 | 3 | import ( 4 | ieigenpod "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/IEigenPod" 5 | ieigenpodmanager "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/IEigenPodManager" 6 | "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth" 7 | "github.com/Layr-Labs/eigensdk-go/chainio/txmgr" 8 | "github.com/Layr-Labs/eigensdk-go/logging" 9 | "github.com/Layr-Labs/eigensdk-go/utils" 10 | 11 | "github.com/ethereum/go-ethereum/common" 12 | ) 13 | 14 | type ChainWriter struct { 15 | logger logging.Logger 16 | ethClient eth.HttpBackend 17 | eigenPod *ieigenpod.IEigenPod 18 | txMgr txmgr.TxManager 19 | } 20 | 21 | type ManagerChainWriter struct { 22 | logger logging.Logger 23 | ethClient eth.HttpBackend 24 | manager *ieigenpodmanager.IEigenPodManager 25 | txMgr txmgr.TxManager 26 | } 27 | 28 | func newChainWriter( 29 | eigenPod *ieigenpod.IEigenPod, 30 | ethClient eth.HttpBackend, 31 | logger logging.Logger, 32 | txMgr txmgr.TxManager, 33 | ) *ChainWriter { 34 | logger = logger.With(logging.ComponentKey, "eigenpod/writer") 35 | 36 | return &ChainWriter{ 37 | logger: logger, 38 | ethClient: ethClient, 39 | eigenPod: eigenPod, 40 | txMgr: txMgr, 41 | } 42 | } 43 | 44 | func newManagerChainWriter( 45 | manager *ieigenpodmanager.IEigenPodManager, 46 | ethClient eth.HttpBackend, 47 | logger logging.Logger, 48 | txMgr txmgr.TxManager, 49 | ) *ManagerChainWriter { 50 | logger = logger.With(logging.ComponentKey, "eigenpodmanager/writer") 51 | 52 | return &ManagerChainWriter{ 53 | logger: logger, 54 | ethClient: ethClient, 55 | manager: manager, 56 | txMgr: txMgr, 57 | } 58 | } 59 | 60 | func NewWriter( 61 | eigenPodAddress common.Address, 62 | ethClient eth.HttpBackend, 63 | txMgr txmgr.TxManager, 64 | logger logging.Logger, 65 | ) (*ChainWriter, error) { 66 | pod, err := ieigenpod.NewIEigenPod(eigenPodAddress, ethClient) 67 | if err != nil { 68 | return nil, utils.WrapError("Failed to create EigenPod contract", err) 69 | } 70 | 71 | return newChainWriter(pod, ethClient, logger, txMgr), nil 72 | } 73 | 74 | func NewManagerWriter( 75 | eigenPodManagerAddress common.Address, 76 | ethClient eth.HttpBackend, 77 | txMgr txmgr.TxManager, 78 | logger logging.Logger, 79 | ) (*ManagerChainWriter, error) { 80 | manager, err := ieigenpodmanager.NewIEigenPodManager(eigenPodManagerAddress, ethClient) 81 | if err != nil { 82 | return nil, utils.WrapError("Failed to create EigenPodManager contract", err) 83 | } 84 | 85 | return newManagerChainWriter(manager, ethClient, logger, txMgr), nil 86 | } 87 | 88 | // TODO(madhur): Add methods to ChainWriter and ManagerChainWriter to interact with the contracts. 89 | -------------------------------------------------------------------------------- /chainio/clients/elcontracts/builder.go: -------------------------------------------------------------------------------- 1 | package elcontracts 2 | 3 | import ( 4 | "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth" 5 | "github.com/Layr-Labs/eigensdk-go/chainio/txmgr" 6 | "github.com/Layr-Labs/eigensdk-go/logging" 7 | "github.com/Layr-Labs/eigensdk-go/metrics" 8 | ) 9 | 10 | // Returns a tuple of reader clients with the given: 11 | // configuration, HTTP client, logger and metrics. 12 | func BuildReadClients( 13 | config Config, 14 | client eth.HttpBackend, 15 | logger logging.Logger, 16 | eigenMetrics *metrics.EigenMetrics, 17 | ) (*ChainReader, *ContractBindings, error) { 18 | elContractBindings, err := NewBindingsFromConfig( 19 | config, 20 | client, 21 | logger, 22 | ) 23 | if err != nil { 24 | return nil, nil, err 25 | } 26 | 27 | elChainReader := NewChainReader( 28 | elContractBindings.DelegationManager, 29 | elContractBindings.StrategyManager, 30 | elContractBindings.AvsDirectory, 31 | elContractBindings.RewardsCoordinator, 32 | elContractBindings.AllocationManager, 33 | elContractBindings.PermissionController, 34 | logger, 35 | client, 36 | ) 37 | 38 | return elChainReader, elContractBindings, nil 39 | } 40 | 41 | func BuildClients( 42 | config Config, 43 | client eth.HttpBackend, 44 | txMgr txmgr.TxManager, 45 | logger logging.Logger, 46 | eigenMetrics *metrics.EigenMetrics, 47 | ) (*ChainReader, *ChainWriter, *ContractBindings, error) { 48 | elContractBindings, err := NewBindingsFromConfig( 49 | config, 50 | client, 51 | logger, 52 | ) 53 | if err != nil { 54 | return nil, nil, nil, err 55 | } 56 | 57 | elChainReader := NewChainReader( 58 | elContractBindings.DelegationManager, 59 | elContractBindings.StrategyManager, 60 | elContractBindings.AvsDirectory, 61 | elContractBindings.RewardsCoordinator, 62 | elContractBindings.AllocationManager, 63 | elContractBindings.PermissionController, 64 | logger, 65 | client, 66 | ) 67 | 68 | elChainWriter := NewChainWriter( 69 | elContractBindings.DelegationManager, 70 | elContractBindings.StrategyManager, 71 | elContractBindings.RewardsCoordinator, 72 | elContractBindings.AvsDirectory, 73 | elContractBindings.AllocationManager, 74 | elContractBindings.PermissionController, 75 | elContractBindings.StrategyManagerAddr, 76 | elContractBindings.DelegationManagerAddr, 77 | elChainReader, 78 | client, 79 | logger, 80 | eigenMetrics, 81 | txMgr, 82 | ) 83 | 84 | return elChainReader, elChainWriter, elContractBindings, nil 85 | } 86 | -------------------------------------------------------------------------------- /chainio/clients/eth/client.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "context" 5 | "math/big" 6 | 7 | "github.com/ethereum/go-ethereum/core/types" 8 | 9 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 10 | ) 11 | 12 | // HttpBackend is the HTTP ETH Client interface 13 | // It is exactly the same as the WsBackend and there is no difference between them to the compiler, 14 | // but we keep them separate as a signal to the programmer that an eth.Client with an underlying http/ws connection is 15 | // expected 16 | type HttpBackend interface { 17 | bind.ContractBackend 18 | 19 | BlockNumber(ctx context.Context) (uint64, error) 20 | BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) 21 | } 22 | 23 | // WsBackend is the Websocket ETH Client interface 24 | // It is exactly the same as the HttpBackend and there is no difference between them to the compiler, 25 | // but we keep them separate as a signal to the programmer that an eth.Client with an underlying http/ws connection is 26 | // expected 27 | type WsBackend interface { 28 | bind.ContractBackend 29 | 30 | BlockNumber(ctx context.Context) (uint64, error) 31 | BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) 32 | } 33 | -------------------------------------------------------------------------------- /chainio/clients/fireblocks/cancel_transaction.go: -------------------------------------------------------------------------------- 1 | package fireblocks 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/Layr-Labs/eigensdk-go/utils" 10 | ) 11 | 12 | type CancelTransactionResponse struct { 13 | Success bool `json:"success"` 14 | } 15 | 16 | func (f *client) CancelTransaction(ctx context.Context, txID string) (bool, error) { 17 | f.logger.Debug("Fireblocks cancel transaction", "txID", txID) 18 | path := fmt.Sprintf("/v1/transactions/%s/cancel", txID) 19 | res, err := f.makeRequest(ctx, "POST", path, nil) 20 | if err != nil { 21 | return false, utils.WrapError("error making request", err) 22 | } 23 | var response CancelTransactionResponse 24 | err = json.NewDecoder(strings.NewReader(string(res))).Decode(&response) 25 | if err != nil { 26 | return false, utils.WrapError("error parsing response body", err) 27 | } 28 | 29 | return response.Success, nil 30 | } 31 | -------------------------------------------------------------------------------- /chainio/clients/fireblocks/contract_call.go: -------------------------------------------------------------------------------- 1 | package fireblocks 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "strings" 7 | 8 | "github.com/Layr-Labs/eigensdk-go/utils" 9 | ) 10 | 11 | func NewContractCallRequest( 12 | externalTxID string, 13 | assetID AssetID, 14 | sourceAccountID string, 15 | destinationAccountID string, 16 | amount string, // amount in ETH 17 | calldata string, 18 | replaceTxByHash string, 19 | gasPrice string, 20 | gasLimit string, 21 | maxFee string, 22 | priorityFee string, 23 | feeLevel FeeLevel, 24 | ) *TransactionRequest { 25 | req := &TransactionRequest{ 26 | Operation: ContractCall, 27 | ExternalTxID: externalTxID, 28 | AssetID: assetID, 29 | Source: account{ 30 | Type: "VAULT_ACCOUNT", 31 | ID: sourceAccountID, 32 | }, 33 | // https://developers.fireblocks.com/reference/transaction-sources-destinations 34 | Destination: account{ 35 | Type: "EXTERNAL_WALLET", 36 | ID: destinationAccountID, 37 | }, 38 | Amount: amount, 39 | ExtraParameters: extraParams{ 40 | Calldata: calldata, 41 | }, 42 | ReplaceTxByHash: replaceTxByHash, 43 | GasLimit: gasLimit, 44 | } 45 | 46 | if maxFee != "" && priorityFee != "" { 47 | req.MaxFee = maxFee 48 | req.PriorityFee = priorityFee 49 | } else if gasPrice != "" { 50 | req.GasPrice = gasPrice 51 | } else { 52 | req.FeeLevel = feeLevel 53 | } 54 | 55 | return req 56 | } 57 | 58 | func (f *client) ContractCall(ctx context.Context, req *TransactionRequest) (*TransactionResponse, error) { 59 | f.logger.Debug("Fireblocks call contract", "req", req) 60 | res, err := f.makeRequest(ctx, "POST", "/v1/transactions", req) 61 | if err != nil { 62 | return nil, utils.WrapError("error making request", err) 63 | } 64 | var response TransactionResponse 65 | err = json.NewDecoder(strings.NewReader(string(res))).Decode(&response) 66 | if err != nil { 67 | return nil, utils.WrapError("error parsing response body", err) 68 | } 69 | 70 | return &TransactionResponse{ 71 | ID: response.ID, 72 | Status: response.Status, 73 | }, nil 74 | } 75 | -------------------------------------------------------------------------------- /chainio/clients/fireblocks/get_asset_addresses.go: -------------------------------------------------------------------------------- 1 | package fireblocks 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | "strings" 9 | 10 | "github.com/Layr-Labs/eigensdk-go/utils" 11 | ) 12 | 13 | type AssetAddress struct { 14 | AssetID AssetID `json:"assetId"` 15 | Address string `json:"address"` 16 | Description string `json:"description"` 17 | Tag string `json:"tag"` 18 | Type string `json:"type"` 19 | CustomerRefID string `json:"customerRefId"` 20 | AddressFormat string `json:"addressFormat"` 21 | LegacyAddress string `json:"legacyAddress"` 22 | EnterpriseAddress string `json:"enterpriseAddress"` 23 | BIP44AddressIndex int `json:"bip44AddressIndex"` 24 | UserDefined bool `json:"userDefined"` 25 | } 26 | 27 | func (f *client) GetAssetAddresses(ctx context.Context, vaultID string, assetID AssetID) ([]AssetAddress, error) { 28 | f.logger.Debug("Fireblocks get asset addressees", "vaultID", vaultID, "assetID", assetID) 29 | var addresses []AssetAddress 30 | type paging struct { 31 | Before string `json:"before"` 32 | After string `json:"after"` 33 | } 34 | var response struct { 35 | Addresses []AssetAddress `json:"addresses"` 36 | Paging paging `json:"paging"` 37 | } 38 | 39 | p := paging{} 40 | next := true 41 | for next { 42 | path := fmt.Sprintf("/v1/vault/accounts/%s/%s/addresses_paginated", vaultID, assetID) 43 | u, err := url.Parse(path) 44 | if err != nil { 45 | return nil, utils.WrapError("error parsing URL", err) 46 | } 47 | q := u.Query() 48 | q.Set("before", p.Before) 49 | q.Set("after", p.After) 50 | u.RawQuery = q.Encode() 51 | 52 | res, err := f.makeRequest(ctx, "GET", u.String(), nil) 53 | if err != nil { 54 | return nil, utils.WrapError("error making request", err) 55 | } 56 | body := string(res) 57 | err = json.NewDecoder(strings.NewReader(body)).Decode(&response) 58 | if err != nil { 59 | text := fmt.Sprintf("error parsing response body: %s", body) 60 | return nil, utils.WrapError(text, err) 61 | } 62 | 63 | addresses = append(addresses, response.Addresses...) 64 | p = response.Paging 65 | if p.After == "" { 66 | next = false 67 | } 68 | } 69 | 70 | return addresses, nil 71 | } 72 | -------------------------------------------------------------------------------- /chainio/clients/fireblocks/get_transaction.go: -------------------------------------------------------------------------------- 1 | package fireblocks 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/Layr-Labs/eigensdk-go/utils" 10 | ) 11 | 12 | // Transaction is a type for the transaction response from Fireblocks 13 | type Transaction struct { 14 | ID string `json:"id"` 15 | ExternalID string `json:"externalId"` 16 | Status TxStatus `json:"status"` 17 | SubStatus string `json:"subStatus"` 18 | TxHash string `json:"txHash"` 19 | Operation TransactionOperation `json:"operation"` 20 | CreatedAt int64 `json:"createdAt"` 21 | LastUpdated int64 `json:"lastUpdated"` 22 | AssetID AssetID `json:"assetId"` 23 | Source account `json:"source"` 24 | SourceAddress string `json:"sourceAddress"` 25 | Destination account `json:"destination"` 26 | DestinationAddress string `json:"destinationAddress"` 27 | DestinationAddressDescription string `json:"destinationAddressDescription"` 28 | DestinationTag string `json:"destinationTag"` 29 | AmountInfo struct { 30 | Amount string `json:"amount"` 31 | RequestedAmount string `json:"requestedAmount"` 32 | NetAmount string `json:"netAmount"` 33 | AmountUSD string `json:"amountUSD"` 34 | } `json:"amountInfo"` 35 | FeeInfo struct { 36 | NetworkFee string `json:"networkFee"` 37 | ServiceFee string `json:"serviceFee"` 38 | GasPrice string `json:"gasPrice"` 39 | } `json:"feeInfo"` 40 | FeeCurrency string `json:"feeCurrency"` 41 | ExtraParameters struct { 42 | ContractCallData string `json:"contractCallData"` 43 | } `json:"extraParameters"` 44 | NumOfConfirmations int `json:"numOfConfirmations"` 45 | // The block hash and height of the block that this transaction was mined in. 46 | BlockInfo struct { 47 | BlockHeight string `json:"blockHeight"` 48 | BlockHash string `json:"blockHash"` 49 | } `json:"blockInfo"` 50 | } 51 | 52 | func (f *client) GetTransaction(ctx context.Context, txID string) (*Transaction, error) { 53 | f.logger.Debug("Fireblocks get transaction", "txID", txID) 54 | path := fmt.Sprintf("/v1/transactions/%s", txID) 55 | res, err := f.makeRequest(ctx, "GET", path, nil) 56 | if err != nil { 57 | return nil, utils.WrapError("error making request", err) 58 | } 59 | var tx Transaction 60 | err = json.NewDecoder(strings.NewReader(string(res))).Decode(&tx) 61 | if err != nil { 62 | return nil, utils.WrapError("error parsing response body", err) 63 | } 64 | 65 | return &tx, nil 66 | } 67 | -------------------------------------------------------------------------------- /chainio/clients/fireblocks/list_contracts.go: -------------------------------------------------------------------------------- 1 | package fireblocks 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/Layr-Labs/eigensdk-go/utils" 10 | "github.com/ethereum/go-ethereum/common" 11 | ) 12 | 13 | type WhitelistedContract struct { 14 | ID string `json:"id"` 15 | Name string `json:"name"` 16 | Assets []struct { 17 | ID AssetID `json:"id"` 18 | Status string `json:"status"` 19 | Address common.Address `json:"address"` 20 | Tag string `json:"tag"` 21 | } `json:"assets"` 22 | } 23 | 24 | func (f *client) ListContracts(ctx context.Context) ([]WhitelistedContract, error) { 25 | var contracts []WhitelistedContract 26 | res, err := f.makeRequest(ctx, "GET", "/v1/contracts", nil) 27 | if err != nil { 28 | return contracts, utils.WrapError("error making request", err) 29 | } 30 | body := string(res) 31 | err = json.NewDecoder(strings.NewReader(body)).Decode(&contracts) 32 | if err != nil { 33 | text := fmt.Sprintf("error parsing response body: %s", body) 34 | return contracts, utils.WrapError(text, err) 35 | } 36 | 37 | return contracts, nil 38 | } 39 | -------------------------------------------------------------------------------- /chainio/clients/fireblocks/list_external_accounts.go: -------------------------------------------------------------------------------- 1 | package fireblocks 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/Layr-Labs/eigensdk-go/utils" 10 | "github.com/ethereum/go-ethereum/common" 11 | ) 12 | 13 | type WhitelistedAccount struct { 14 | ID string `json:"id"` 15 | Name string `json:"name"` 16 | Assets []struct { 17 | ID AssetID `json:"id"` 18 | Balance string `json:"balance"` 19 | LockedAmount string `json:"lockedAmount"` 20 | Status string `json:"status"` 21 | Address common.Address `json:"address"` 22 | Tag string `json:"tag"` 23 | } `json:"assets"` 24 | } 25 | 26 | func (f *client) ListExternalWallets(ctx context.Context) ([]WhitelistedAccount, error) { 27 | var accounts []WhitelistedAccount 28 | res, err := f.makeRequest(ctx, "GET", "/v1/external_wallets", nil) 29 | if err != nil { 30 | return accounts, utils.WrapError("error making request", err) 31 | } 32 | body := string(res) 33 | err = json.NewDecoder(strings.NewReader(body)).Decode(&accounts) 34 | if err != nil { 35 | text := fmt.Sprintf("error parsing response body: %s", body) 36 | return accounts, utils.WrapError(text, err) 37 | } 38 | 39 | return accounts, nil 40 | } 41 | -------------------------------------------------------------------------------- /chainio/clients/fireblocks/list_vault_accounts.go: -------------------------------------------------------------------------------- 1 | package fireblocks 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | "strings" 9 | 10 | "github.com/Layr-Labs/eigensdk-go/utils" 11 | ) 12 | 13 | type Asset struct { 14 | ID AssetID `json:"id"` 15 | Total string `json:"total"` 16 | Balance string `json:"balance"` 17 | Available string `json:"available"` 18 | } 19 | 20 | type VaultAccount struct { 21 | ID string `json:"id"` 22 | Name string `json:"name"` 23 | Assets []Asset `json:"assets"` 24 | } 25 | 26 | func (f *client) ListVaultAccounts(ctx context.Context) ([]VaultAccount, error) { 27 | var accounts []VaultAccount 28 | type paging struct { 29 | Before string `json:"before"` 30 | After string `json:"after"` 31 | } 32 | var response struct { 33 | Accounts []VaultAccount `json:"accounts"` 34 | Paging paging `json:"paging"` 35 | } 36 | p := paging{} 37 | next := true 38 | for next { 39 | u, err := url.Parse("/v1/vault/accounts_paged") 40 | if err != nil { 41 | return accounts, utils.WrapError("error parsing URL", err) 42 | } 43 | q := u.Query() 44 | q.Set("before", p.Before) 45 | q.Set("after", p.After) 46 | u.RawQuery = q.Encode() 47 | res, err := f.makeRequest(ctx, "GET", u.String(), nil) 48 | if err != nil { 49 | return accounts, utils.WrapError("error making request", err) 50 | } 51 | body := string(res) 52 | err = json.NewDecoder(strings.NewReader(body)).Decode(&response) 53 | if err != nil { 54 | text := fmt.Sprintf("error parsing response body: %s", body) 55 | return accounts, utils.WrapError(text, err) 56 | } 57 | 58 | accounts = append(accounts, response.Accounts...) 59 | p = response.Paging 60 | if p.After == "" { 61 | next = false 62 | } 63 | } 64 | 65 | return accounts, nil 66 | } 67 | -------------------------------------------------------------------------------- /chainio/clients/fireblocks/status.go: -------------------------------------------------------------------------------- 1 | package fireblocks 2 | 3 | type TxStatus string 4 | 5 | // statuses for transactions 6 | // ref: https://developers.fireblocks.com/reference/primary-transaction-statuses 7 | const ( 8 | Submitted TxStatus = "SUBMITTED" 9 | PendingScreening TxStatus = "PENDING_AML_SCREENING" 10 | PendingAuthorization TxStatus = "PENDING_AUTHORIZATION" 11 | Queued TxStatus = "QUEUED" 12 | PendingSignature TxStatus = "PENDING_SIGNATURE" 13 | PendingEmailApproval TxStatus = "PENDING_3RD_PARTY_MANUAL_APPROVAL" 14 | Pending3rdParty TxStatus = "PENDING_3RD_PARTY" 15 | Broadcasting TxStatus = "BROADCASTING" 16 | Confirming TxStatus = "CONFIRMING" 17 | Completed TxStatus = "COMPLETED" 18 | Cancelling TxStatus = "CANCELLING" 19 | Cancelled TxStatus = "CANCELLED" 20 | Blocked TxStatus = "BLOCKED" 21 | Rejected TxStatus = "REJECTED" 22 | Failed TxStatus = "FAILED" 23 | ) 24 | -------------------------------------------------------------------------------- /chainio/clients/fireblocks/transaction.go: -------------------------------------------------------------------------------- 1 | package fireblocks 2 | 3 | type TransactionOperation string 4 | type FeeLevel string 5 | 6 | const ( 7 | ContractCall TransactionOperation = "CONTRACT_CALL" 8 | Transfer TransactionOperation = "TRANSFER" 9 | Mint TransactionOperation = "MINT" 10 | Burn TransactionOperation = "BURN" 11 | TypedMessage TransactionOperation = "TYPED_MESSAGE" 12 | Raw TransactionOperation = "RAW" 13 | 14 | FeeLevelHigh FeeLevel = "HIGH" 15 | FeeLevelMedium FeeLevel = "MEDIUM" 16 | FeeLevelLow FeeLevel = "LOW" 17 | ) 18 | 19 | type account struct { 20 | Type string `json:"type"` 21 | ID string `json:"id"` 22 | } 23 | 24 | type extraParams struct { 25 | Calldata string `json:"contractCallData"` 26 | } 27 | 28 | type TransactionRequest struct { 29 | Operation TransactionOperation `json:"operation"` 30 | // ExternalTxID is an optional field that can be used as an idempotency key. 31 | ExternalTxID string `json:"externalTxId,omitempty"` 32 | AssetID AssetID `json:"assetId"` 33 | Source account `json:"source"` 34 | Destination account `json:"destination"` 35 | Amount string `json:"amount,omitempty"` 36 | ExtraParameters extraParams `json:"extraParameters"` 37 | // In case a transaction is stuck, specify the hash of the stuck transaction to replace it 38 | // by this transaction with a higher fee, or to replace it with this transaction with a zero fee and drop it from 39 | // the blockchain. 40 | ReplaceTxByHash string `json:"replaceTxByHash,omitempty"` 41 | // GasPrice and GasLimit are the gas price and gas limit for the transaction. 42 | // If GasPrice is specified (non-1559), MaxFee and PriorityFee are not required. 43 | GasPrice string `json:"gasPrice,omitempty"` 44 | GasLimit string `json:"gasLimit,omitempty"` 45 | // MaxFee and PriorityFee are the maximum and priority fees for the transaction. 46 | // If the transaction is stuck, the Fireblocks platform will replace the transaction with a new one with a higher 47 | // fee. 48 | // These fields are required if FeeLevel is not specified. 49 | MaxFee string `json:"maxFee,omitempty"` 50 | PriorityFee string `json:"priorityFee,omitempty"` 51 | // FeeLevel is the fee level for the transaction which Fireblocks estimates based on the current network conditions. 52 | // The fee level can be HIGH, MEDIUM, or LOW. 53 | // If MaxFee and PriorityFee are not specified, the Fireblocks platform will use the default fee level MEDIUM. 54 | // Ref: https://developers.fireblocks.com/docs/gas-estimation#estimated-network-fee 55 | FeeLevel FeeLevel `json:"feeLevel,omitempty"` 56 | } 57 | 58 | type TransactionResponse struct { 59 | ID string `json:"id"` 60 | Status TxStatus `json:"status"` 61 | } 62 | -------------------------------------------------------------------------------- /chainio/clients/fireblocks/transfer.go: -------------------------------------------------------------------------------- 1 | package fireblocks 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "strings" 7 | 8 | "github.com/Layr-Labs/eigensdk-go/utils" 9 | ) 10 | 11 | func NewTransferRequest( 12 | externalTxID string, 13 | assetID AssetID, 14 | sourceAccountID string, 15 | destinationAccountID string, 16 | amount string, // amount in ETH 17 | replaceTxByHash string, 18 | gasPrice string, 19 | gasLimit string, 20 | maxFee string, 21 | priorityFee string, 22 | feeLevel FeeLevel, 23 | ) *TransactionRequest { 24 | req := &TransactionRequest{ 25 | Operation: Transfer, 26 | ExternalTxID: externalTxID, 27 | AssetID: assetID, 28 | Source: account{ 29 | Type: "VAULT_ACCOUNT", 30 | ID: sourceAccountID, 31 | }, 32 | // https://developers.fireblocks.com/reference/transaction-sources-destinations 33 | Destination: account{ 34 | Type: "EXTERNAL_WALLET", 35 | ID: destinationAccountID, 36 | }, 37 | Amount: amount, 38 | ReplaceTxByHash: replaceTxByHash, 39 | GasLimit: gasLimit, 40 | } 41 | 42 | if maxFee != "" && priorityFee != "" { 43 | req.MaxFee = maxFee 44 | req.PriorityFee = priorityFee 45 | } else if gasPrice != "" { 46 | req.GasPrice = gasPrice 47 | } else { 48 | req.FeeLevel = feeLevel 49 | } 50 | 51 | return req 52 | } 53 | 54 | func (f *client) Transfer(ctx context.Context, req *TransactionRequest) (*TransactionResponse, error) { 55 | f.logger.Debug("Fireblocks transfer", "req", req) 56 | res, err := f.makeRequest(ctx, "POST", "/v1/transactions", req) 57 | if err != nil { 58 | return nil, utils.WrapError("error making request", err) 59 | } 60 | var response TransactionResponse 61 | err = json.NewDecoder(strings.NewReader(string(res))).Decode(&response) 62 | if err != nil { 63 | return nil, utils.WrapError("error parsing response body", err) 64 | } 65 | 66 | return &TransactionResponse{ 67 | ID: response.ID, 68 | Status: response.Status, 69 | }, nil 70 | } 71 | -------------------------------------------------------------------------------- /chainio/clients/wallet/README.md: -------------------------------------------------------------------------------- 1 | # Wallet 2 | 3 | TODO -------------------------------------------------------------------------------- /chainio/clients/wallet/privatekey_wallet.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/Layr-Labs/eigensdk-go/logging" 8 | "github.com/Layr-Labs/eigensdk-go/signerv2" 9 | "github.com/Layr-Labs/eigensdk-go/utils" 10 | "github.com/ethereum/go-ethereum/common" 11 | "github.com/ethereum/go-ethereum/core/types" 12 | ) 13 | 14 | type EthBackend interface { 15 | SendTransaction(ctx context.Context, tx *types.Transaction) error 16 | TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) 17 | } 18 | 19 | type privateKeyWallet struct { 20 | ethClient EthBackend 21 | address common.Address 22 | signerFn signerv2.SignerFn 23 | logger logging.Logger 24 | } 25 | 26 | var _ Wallet = (*privateKeyWallet)(nil) 27 | 28 | func NewPrivateKeyWallet( 29 | ethClient EthBackend, 30 | signer signerv2.SignerFn, 31 | signerAddress common.Address, 32 | logger logging.Logger, 33 | ) (Wallet, error) { 34 | return &privateKeyWallet{ 35 | ethClient: ethClient, 36 | address: signerAddress, 37 | signerFn: signer, 38 | logger: logger, 39 | }, nil 40 | } 41 | 42 | func (t *privateKeyWallet) SendTransaction(ctx context.Context, tx *types.Transaction) (TxID, error) { 43 | 44 | t.logger.Debug("Getting signer for tx") 45 | signer, err := t.signerFn(ctx, t.address) 46 | if err != nil { 47 | return "", err 48 | } 49 | 50 | t.logger.Debug("Sending transaction") 51 | signedTx, err := signer(t.address, tx) 52 | if err != nil { 53 | text := fmt.Sprintf("sign: tx %v failed.", tx.Hash().String()) 54 | return "", utils.WrapError(text, err) 55 | } 56 | 57 | err = t.ethClient.SendTransaction(ctx, signedTx) 58 | if err != nil { 59 | text := fmt.Sprintf("send: tx %v failed.", tx.Hash().String()) 60 | return "", utils.WrapError(text, err) 61 | } 62 | 63 | return signedTx.Hash().Hex(), nil 64 | } 65 | 66 | func (t *privateKeyWallet) GetTransactionReceipt(ctx context.Context, txID TxID) (*types.Receipt, error) { 67 | txHash := common.HexToHash(txID) 68 | return t.ethClient.TransactionReceipt(ctx, txHash) 69 | } 70 | 71 | func (t *privateKeyWallet) SenderAddress(ctx context.Context) (common.Address, error) { 72 | return t.address, nil 73 | } 74 | -------------------------------------------------------------------------------- /chainio/clients/wallet/privatekey_wallet_test.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "context" 5 | "math/big" 6 | "testing" 7 | "time" 8 | 9 | "github.com/Layr-Labs/eigensdk-go/signerv2" 10 | "github.com/Layr-Labs/eigensdk-go/testutils" 11 | "github.com/ethereum/go-ethereum/core/types" 12 | "github.com/ethereum/go-ethereum/crypto" 13 | "github.com/ethereum/go-ethereum/ethclient" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | var ( 18 | chainId = big.NewInt(31337) 19 | ) 20 | 21 | func TestPrivateKeyWallet(t *testing.T) { 22 | logger := testutils.NewTestLogger() 23 | 24 | t.Run("SendTransaction + GetTransactionReceipt", func(t *testing.T) { 25 | anvilC, err := testutils.StartAnvilContainer("") 26 | require.NoError(t, err) 27 | ctxWithTimeout, cancel := context.WithTimeout(context.Background(), 5*time.Second) 28 | defer cancel() 29 | anvilHttpEndpoint, err := anvilC.Endpoint(ctxWithTimeout, "http") 30 | require.NoError(t, err) 31 | ethClient, err := ethclient.Dial(anvilHttpEndpoint) 32 | require.NoError(t, err) 33 | 34 | ecdsaPrivKey, err := crypto.HexToECDSA(testutils.ANVIL_FIRST_PRIVATE_KEY) 35 | require.NoError(t, err) 36 | signerV2, signerAddr, err := signerv2.SignerFromConfig(signerv2.Config{PrivateKey: ecdsaPrivKey}, chainId) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | skWallet, err := NewPrivateKeyWallet(ethClient, signerV2, signerAddr, logger) 42 | require.NoError(t, err) 43 | 44 | tx := types.NewTx(&types.DynamicFeeTx{ 45 | ChainID: chainId, 46 | Nonce: 0, 47 | GasTipCap: big.NewInt(1), 48 | GasFeeCap: big.NewInt(1_000_000_000), 49 | Gas: 21000, 50 | To: &signerAddr, 51 | Value: big.NewInt(1), 52 | }) 53 | ctxWithTimeout, cancel = context.WithTimeout(context.Background(), 5*time.Second) 54 | defer cancel() 55 | txId, err := skWallet.SendTransaction(ctxWithTimeout, tx) 56 | require.NoError(t, err) 57 | 58 | // need to give some time for anvil to process the tx and mine the block 59 | // TODO: shall we expose a public WaitForTxReceipt function in the wallet interface, or somewhere else? 60 | time.Sleep(3 * time.Second) 61 | 62 | ctxWithTimeout, cancel = context.WithTimeout(context.Background(), 5*time.Second) 63 | defer cancel() 64 | receipt, err := skWallet.GetTransactionReceipt(ctxWithTimeout, txId) 65 | require.NoError(t, err) 66 | // make sure the txHash in the mined tx receipt matches the once we sent 67 | require.Equal(t, txId, receipt.TxHash.String()) 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /chainio/clients/wallet/wallet.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/ethereum/go-ethereum/core/types" 8 | ) 9 | 10 | type TxID = string 11 | 12 | // Wallet is an interface for signing and sending transactions to the txpool. 13 | // For a higher-level interface that includes nonce management and gas bumping, use the TxManager interface. 14 | // This interface is used to abstract the process of sending transactions to the Ethereum network 15 | // For example, for an MPC signer, the transaction would be broadcasted via an external API endpoint 16 | // and the status is tracked via another external endpoint instead of being broadcasted 17 | // and retrieved via an Ethereum client. 18 | type Wallet interface { 19 | SendTransaction(ctx context.Context, tx *types.Transaction) (TxID, error) 20 | GetTransactionReceipt(ctx context.Context, txID TxID) (*types.Receipt, error) 21 | // SenderAddress returns the address of the wallet 22 | SenderAddress(ctx context.Context) (common.Address, error) 23 | } 24 | -------------------------------------------------------------------------------- /chainio/gen.go: -------------------------------------------------------------------------------- 1 | package chainio 2 | 3 | //go:generate mockgen -destination=./clients/mocks/fireblocks.go -package=mocks -mock_names=Client=MockFireblocksClient github.com/Layr-Labs/eigensdk-go/chainio/clients/fireblocks Client 4 | //go:generate mockgen -destination=./clients/mocks/wallet.go -package=mocks github.com/Layr-Labs/eigensdk-go/chainio/clients/wallet Wallet 5 | -------------------------------------------------------------------------------- /chainio/txmgr/README.md: -------------------------------------------------------------------------------- 1 | ## Transaction Manager 2 | 3 | Transaction Manager is responsible for 4 | * Building transactions 5 | * Estimating fees and adding gas limit buffer 6 | * Signing transactions 7 | * Sending transactions to the network 8 | * Doing transaction nonce and gas price management to ensure transactions are mined 9 | 10 | 11 | Here's the flow of the simple transaction manager which is used to send smart contract transactions to the network. 12 | ![Simple Transaction Manager](./simple-tx-manager-flow.png) 13 | 14 | ### Simple Transaction Manager 15 | 16 | The simple txmgr simply sends transactions to the network, waits for them to be mined, and returns the receipt. It doesn't do any managing. 17 | 18 | ### Geometric Transaction Manager 19 | 20 | The geometric txmgr is a more advanced version of the simple txmgr. It sends transactions to the network, waits for them to be mined, and if they are not mined within a certain time, it bumps the gas price geometrically and resubmits the transaction. This process is repeated until the transaction is mined. -------------------------------------------------------------------------------- /chainio/txmgr/geometric/geometric_example_test.go: -------------------------------------------------------------------------------- 1 | package geometric 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "fmt" 7 | "math/big" 8 | "os" 9 | 10 | "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth" 11 | "github.com/Layr-Labs/eigensdk-go/chainio/clients/wallet" 12 | "github.com/Layr-Labs/eigensdk-go/logging" 13 | "github.com/Layr-Labs/eigensdk-go/signerv2" 14 | "github.com/Layr-Labs/eigensdk-go/testutils" 15 | "github.com/Layr-Labs/eigensdk-go/utils" 16 | "github.com/ethereum/go-ethereum/common" 17 | "github.com/ethereum/go-ethereum/core/types" 18 | "github.com/ethereum/go-ethereum/crypto" 19 | "github.com/ethereum/go-ethereum/ethclient" 20 | "github.com/prometheus/client_golang/prometheus" 21 | ) 22 | 23 | var ( 24 | chainid = big.NewInt(31337) 25 | ) 26 | 27 | func createTx(client eth.HttpBackend, address common.Address) (*types.Transaction, error) { 28 | zeroAddr := common.HexToAddress("0x0") 29 | nonce, err := client.PendingNonceAt(context.TODO(), address) 30 | if err != nil { 31 | return nil, utils.WrapError("Failed to get PendingNonceAt", err) 32 | } 33 | return types.NewTx(&types.DynamicFeeTx{ 34 | To: &zeroAddr, 35 | Nonce: nonce, 36 | }), nil 37 | } 38 | 39 | func createTxMgr(rpcUrl string, ecdsaPrivateKey *ecdsa.PrivateKey) (eth.HttpBackend, *GeometricTxManager, error) { 40 | logger := logging.NewTextSLogger(os.Stdout, &logging.SLoggerOptions{}) 41 | client, err := ethclient.Dial(rpcUrl) 42 | if err != nil { 43 | return nil, nil, err 44 | } 45 | signerV2, signerAddr, err := signerv2.SignerFromConfig(signerv2.Config{PrivateKey: ecdsaPrivateKey}, chainid) 46 | if err != nil { 47 | return nil, nil, err 48 | } 49 | wallet, err := wallet.NewPrivateKeyWallet(client, signerV2, signerAddr, logger) 50 | if err != nil { 51 | return nil, nil, err 52 | } 53 | reg := prometheus.NewRegistry() 54 | metrics := NewMetrics(reg, "example", logger) 55 | return client, NewGeometricTxnManager(client, wallet, logger, metrics, GeometricTxnManagerParams{}), nil 56 | } 57 | 58 | func ExampleGeometricTxManager() { 59 | anvilC, err := testutils.StartAnvilContainer("") 60 | if err != nil { 61 | fmt.Fprintln(os.Stderr, err) 62 | os.Exit(1) 63 | } 64 | anvilUrl, err := anvilC.Endpoint(context.TODO(), "http") 65 | if err != nil { 66 | fmt.Fprintln(os.Stderr, err) 67 | os.Exit(1) 68 | } 69 | 70 | ecdsaPrivateKey, err := crypto.HexToECDSA(testutils.ANVIL_FIRST_PRIVATE_KEY) 71 | if err != nil { 72 | fmt.Fprintln(os.Stderr, err) 73 | os.Exit(1) 74 | } 75 | pk := ecdsaPrivateKey.PublicKey 76 | address := crypto.PubkeyToAddress(pk) 77 | 78 | client, txmgr, err := createTxMgr(anvilUrl, ecdsaPrivateKey) 79 | if err != nil { 80 | fmt.Fprintln(os.Stderr, err) 81 | os.Exit(1) 82 | } 83 | 84 | tx, err := createTx(client, address) 85 | if err != nil { 86 | fmt.Fprintln(os.Stderr, err) 87 | os.Exit(1) 88 | } 89 | 90 | _, err = txmgr.Send(context.TODO(), tx, true) 91 | if err != nil { 92 | fmt.Fprintln(os.Stderr, err) 93 | os.Exit(1) 94 | } 95 | 96 | // we just add this to make sure the example runs 97 | fmt.Println("Tx sent") 98 | // Output: Tx sent 99 | } 100 | -------------------------------------------------------------------------------- /chainio/txmgr/simple-tx-manager-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Layr-Labs/eigensdk-go/0cca279b4e1231bd5ae59296d84967cb86ea0f6a/chainio/txmgr/simple-tx-manager-flow.png -------------------------------------------------------------------------------- /chainio/txmgr/txmgr.go: -------------------------------------------------------------------------------- 1 | package txmgr 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 7 | "github.com/ethereum/go-ethereum/core/types" 8 | ) 9 | 10 | type TxManager interface { 11 | // Send is used to sign and send a transaction to an evm chain 12 | // It takes an unsigned transaction and then signs it before sending 13 | // It might also take care of nonce management and gas estimation, depending on the implementation 14 | Send(ctx context.Context, tx *types.Transaction, waitForReceipt bool) (*types.Receipt, error) 15 | 16 | // GetNoSendTxOpts generates a TransactOpts with 17 | // - NoSend=true: b/c we want to manage the sending ourselves 18 | // - Signer=NoopSigner: b/c we want the wallet to manage signing 19 | // - From=sender: unfortunately needed as first parameter to 20 | // This is needed when using abigen to construct transactions. 21 | // this to generate the transaction without actually sending it 22 | GetNoSendTxOpts() (*bind.TransactOpts, error) 23 | } 24 | -------------------------------------------------------------------------------- /chainio/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/big" 5 | 6 | m2regcoord "github.com/Layr-Labs/eigensdk-go/M2-contracts/bindings/RegistryCoordinator" 7 | regcoord "github.com/Layr-Labs/eigensdk-go/contracts/bindings/RegistryCoordinator" 8 | "github.com/Layr-Labs/eigensdk-go/crypto/bls" 9 | "github.com/consensys/gnark-crypto/ecc/bn254" 10 | ) 11 | 12 | // BINDING UTILS - conversion from contract structs to golang structs 13 | 14 | func ConvertBn254GethToGnark(input regcoord.BN254G1Point) *bn254.G1Affine { 15 | return bls.NewG1Point(input.X, input.Y).G1Affine 16 | } 17 | 18 | // BN254.sol is a library, so bindings for G1 Points and G2 Points are only generated 19 | // in every contract that imports that library. Thus the output here will need to be 20 | // type casted if G1Point is needed to interface with another contract (eg: BLSPublicKeyCompendium.sol) 21 | func ConvertToBN254G1Point(input *bls.G1Point) regcoord.BN254G1Point { 22 | output := regcoord.BN254G1Point{ 23 | X: input.X.BigInt(big.NewInt(0)), 24 | Y: input.Y.BigInt(big.NewInt(0)), 25 | } 26 | return output 27 | } 28 | 29 | func ConvertToBN254G2Point(input *bls.G2Point) regcoord.BN254G2Point { 30 | output := regcoord.BN254G2Point{ 31 | X: [2]*big.Int{input.X.A1.BigInt(big.NewInt(0)), input.X.A0.BigInt(big.NewInt(0))}, 32 | Y: [2]*big.Int{input.Y.A1.BigInt(big.NewInt(0)), input.Y.A0.BigInt(big.NewInt(0))}, 33 | } 34 | return output 35 | } 36 | 37 | // This function is for M2 functionality 38 | func ConvertM2Bn254GethToGnark(input m2regcoord.BN254G1Point) *bn254.G1Affine { 39 | return bls.NewG1Point(input.X, input.Y).G1Affine 40 | } 41 | 42 | // This function is for M2 functionality 43 | func ConvertToM2BN254G1Point(input *bls.G1Point) m2regcoord.BN254G1Point { 44 | output := m2regcoord.BN254G1Point{ 45 | X: input.X.BigInt(big.NewInt(0)), 46 | Y: input.Y.BigInt(big.NewInt(0)), 47 | } 48 | return output 49 | } 50 | 51 | // This function is for M2 functionality 52 | func ConvertToM2BN254G2Point(input *bls.G2Point) m2regcoord.BN254G2Point { 53 | output := m2regcoord.BN254G2Point{ 54 | X: [2]*big.Int{input.X.A1.BigInt(big.NewInt(0)), input.X.A0.BigInt(big.NewInt(0))}, 55 | Y: [2]*big.Int{input.Y.A1.BigInt(big.NewInt(0)), input.Y.A0.BigInt(big.NewInt(0))}, 56 | } 57 | return output 58 | } 59 | -------------------------------------------------------------------------------- /cmd/egnaddrs/README.md: -------------------------------------------------------------------------------- 1 | ## egnaddrs 2 | This tool is used to help debug and test eigenlayer/avs deployments and contract setups. 3 | 4 | ### How to install 5 | #### Install from repository 6 | ```bash 7 | go install github.com/Layr-Labs/eigensdk-go/cmd/egnaddrs@latest 8 | ``` 9 | 10 | #### Install from local source 11 | If you have git cloned eigensdk-go to your machine, navigate to [cmd/egnaddrs](.) directory and run 12 | ```bash 13 | go install 14 | ``` 15 | 16 | ### Usage 17 | 18 | Currently egnaddrs only supports deriving contract addresses starting from a registry-coordinator or service-manager address. It then prints the following datastructure: 19 | 20 | ```bash 21 | $$$ egnaddrs --registry-coordinator 0x9E545E3C0baAB3E08CdfD552C960A1050f373042 22 | { 23 | "avs": { 24 | "bls-pubkey-compendium (shared)": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44", 25 | "bls-pubkey-registry": "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9", 26 | "index-registry": "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8", 27 | "registry-coordinator": "0x9E545E3C0baAB3E08CdfD552C960A1050f373042", 28 | "service-manager": "0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690", 29 | "stake-registry": "0x851356ae760d987E095750cCeb3bC6014560891C" 30 | }, 31 | "eigenlayer": { 32 | "delegation-manager": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", 33 | "slasher": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", 34 | "strategy-manager": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" 35 | }, 36 | "network": { 37 | "chain-id": "31337", 38 | "rpc-url": "http://localhost:8545" 39 | } 40 | } 41 | ``` 42 | 43 | Note: If working with the slashing version of contracts, in case the service manager address is not passed, then the returned service manager address will be zero. 44 | -------------------------------------------------------------------------------- /cmd/egnaddrs/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/Layr-Labs/eigensdk-go/testutils" 8 | "github.com/ethereum/go-ethereum/common" 9 | ) 10 | 11 | const ( 12 | anvilStateFileName = "contracts-deployed-anvil-state.json" // in contracts/anvil/ 13 | ) 14 | 15 | func TestEgnAddrsWithServiceManagerFlag(t *testing.T) { 16 | 17 | anvilC, err := testutils.StartAnvilContainer(anvilStateFileName) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | anvilEndpoint, err := anvilC.Endpoint(context.Background(), "http") 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | // read input from JSON if available, otherwise use default values 27 | var defaultInput = struct { 28 | ServiceManagerAddress common.Address `json:"service_manager_address"` 29 | RpcUrl string `json:"rpc_url"` 30 | }{ 31 | ServiceManagerAddress: testutils.GetContractAddressesFromContractRegistry(anvilEndpoint).ServiceManager, 32 | RpcUrl: anvilEndpoint, 33 | } 34 | testData := testutils.NewTestData(defaultInput) 35 | 36 | args := []string{"egnaddrs"} 37 | args = append(args, "--service-manager", testData.Input.ServiceManagerAddress.Hex()) 38 | args = append(args, "--rpc-url", testData.Input.RpcUrl) 39 | // we just make sure it doesn't crash 40 | run(args) 41 | } 42 | 43 | func TestEgnAddrsWithRegistryCoordinatorFlag(t *testing.T) { 44 | 45 | anvilC, err := testutils.StartAnvilContainer(anvilStateFileName) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | anvilEndpoint, err := anvilC.Endpoint(context.Background(), "http") 50 | if err != nil { 51 | t.Error(err) 52 | } 53 | contractAddrs := testutils.GetContractAddressesFromContractRegistry(anvilEndpoint) 54 | 55 | args := []string{"egnaddrs"} 56 | args = append(args, "--registry-coordinator", contractAddrs.RegistryCoordinator.Hex()) 57 | args = append(args, "--rpc-url", anvilEndpoint) 58 | // we just make sure it doesn't crash 59 | run(args) 60 | } 61 | -------------------------------------------------------------------------------- /cmd/egnkey/README.md: -------------------------------------------------------------------------------- 1 | ## egnkey 2 | This tool is used to manage keys for AVS development purpose 3 | 4 | Features: 5 | - [Generate ecdsa or bls key in batches](#generate-ecdsa-or-bls-key-in-batches) 6 | 7 | ### How to install 8 | #### Install from source 9 | ```bash 10 | go install github.com/Layr-Labs/eigensdk-go/cmd/egnkey@latest 11 | ``` 12 | 13 | #### Build from source 14 | Navigate to [egnkey](../egnkey/) directory and run 15 | ```bash 16 | go install 17 | ``` 18 | 19 | ### Generate ecdsa or bls key in batches 20 | 21 | To create in a random folder 22 | ```bash 23 | egnkey generate --key-type ecdsa --num-keys 24 | ``` 25 | 26 | To create in specific folder 27 | ```bash 28 | egnkey generate --key-type ecdsa --num-keys --output-dir 29 | ``` 30 | 31 | To create `ECDSA` and `BLS` keys in a random folder 32 | ```bash 33 | egnkey generate --key-type both --num-keys 34 | ``` -------------------------------------------------------------------------------- /cmd/egnkey/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/Layr-Labs/eigensdk-go/cmd/egnkey/generate" 8 | "github.com/Layr-Labs/eigensdk-go/cmd/egnkey/operatorid" 9 | "github.com/Layr-Labs/eigensdk-go/cmd/egnkey/store" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | func main() { 14 | err := run(os.Args) 15 | if err != nil { 16 | fmt.Println("Error: ", err) 17 | os.Exit(1) 18 | } 19 | } 20 | 21 | // We structure run in this way to make it testable. 22 | // see https://github.com/urfave/cli/issues/731 23 | func run(args []string) error { 24 | app := cli.NewApp() 25 | app.Name = "egnkey" 26 | app.Description = "Eigenlayer batch keys manager" 27 | app.Commands = []*cli.Command{ 28 | generate.Command, 29 | store.Command, 30 | operatorid.Command, 31 | } 32 | app.Usage = "Used to manage batch keys for testing" 33 | 34 | err := app.Run(args) 35 | return err 36 | } 37 | -------------------------------------------------------------------------------- /cmd/egnkey/operatorid/operatorid.go: -------------------------------------------------------------------------------- 1 | package operatorid 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | 7 | "github.com/Layr-Labs/eigensdk-go/crypto/bls" 8 | "github.com/Layr-Labs/eigensdk-go/types" 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | var ( 13 | PrivateKey = &cli.StringFlag{ 14 | Name: "private-key", 15 | Usage: "(bn254) private key from which to derive operatorId from", 16 | Required: true, 17 | } 18 | ) 19 | 20 | var Command = &cli.Command{ 21 | Name: "derive-operator-id", 22 | Aliases: []string{"d"}, 23 | Description: `Given a private key, output its associated operatorId (hash of bn254 G1 pubkey).`, 24 | Action: derive, 25 | Flags: []cli.Flag{ 26 | PrivateKey, 27 | }, 28 | } 29 | 30 | func derive(c *cli.Context) error { 31 | sk := c.String(PrivateKey.Name) 32 | keypair, err := bls.NewKeyPairFromString(sk) 33 | if err != nil { 34 | return err 35 | } 36 | operatorId := types.OperatorIdFromKeyPair(keypair) 37 | fmt.Println("0x" + hex.EncodeToString(operatorId[:])) 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /cmd/egnkey/store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "github.com/Layr-Labs/eigensdk-go/crypto/ecdsa" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | const defaultFile = "key.json" 9 | 10 | var ( 11 | PrivateKey = &cli.StringFlag{ 12 | Name: "private-key", 13 | Usage: "private key to store (in hex)", 14 | Required: true, 15 | } 16 | OutputFile = &cli.StringFlag{ 17 | Name: "file", 18 | Usage: "file to store key", 19 | Required: false, 20 | } 21 | Password = &cli.StringFlag{ 22 | Name: "password", 23 | Usage: "password to encrypt key", 24 | Required: false, 25 | } 26 | ) 27 | 28 | var Command = &cli.Command{ 29 | Name: "convert", 30 | Aliases: []string{"c"}, 31 | Description: `Stores an ecdsa key to a file, in web3 secret storage format.`, 32 | Action: store, 33 | Flags: []cli.Flag{ 34 | PrivateKey, 35 | OutputFile, 36 | Password, 37 | }, 38 | } 39 | 40 | func store(c *cli.Context) error { 41 | outputFile := c.String(OutputFile.Name) 42 | if outputFile == "" { 43 | outputFile = defaultFile 44 | } 45 | password := c.String(Password.Name) 46 | err := ecdsa.WriteKeyFromHex(outputFile, c.String(PrivateKey.Name), password) 47 | if err != nil { 48 | return err 49 | } 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /contracts/.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 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /contracts/abigen-with-interfaces.Dockerfile: -------------------------------------------------------------------------------- 1 | # use the below command to build this docker image locally 2 | # docker build -t abigen-with-interfaces -f abigen-with-interfaces.Dockerfile . 3 | FROM golang:1.21 as build 4 | 5 | # this installs our fork of abigen with also creates interfaces for the binding structs 6 | # See https://github.com/ethereum/go-ethereum/pull/28279 7 | RUN apt update && \ 8 | apt install git -y && \ 9 | git clone https://github.com/samlaf/go-ethereum.git && \ 10 | cd go-ethereum/cmd/abigen && \ 11 | git checkout issue-28278/abigen-interfaces && \ 12 | go build . 13 | 14 | 15 | FROM debian:latest 16 | COPY --from=build /go/go-ethereum/cmd/abigen/abigen /usr/local/bin/abigen 17 | ENTRYPOINT [ "abigen"] -------------------------------------------------------------------------------- /contracts/anvil/README.md: -------------------------------------------------------------------------------- 1 | # Integration Tests 2 | 3 | We store an anvil state files in this directory, so that we can start an anvil chain with the correct state for integration tests. 4 | ``` 5 | anvil --load-state STATE_FILE.json 6 | ``` 7 | 8 | ## Eigenlayer deployment state file 9 | `eigenlayer-deployed-anvil-state.json` contains the eigenlayer deployment. 10 | 11 | It was created by running this [deploy script](https://github.com/Layr-Labs/eigenlayer-contracts/blob/2cb9ed107c6c918b9dfbac94cd71b4ab7c94e8c2/script/testing/M2_Deploy_From_Scratch.s.sol). If you ever need to redeploy a new version of eigenlayer contracts, first start an anvil chain that dumps its state after exiting 12 | ``` 13 | anvil --dump-state eigenlayer-deployed-anvil-state.json 14 | ``` 15 | Then run the deploy script 16 | ``` 17 | forge script script/testing/M2_Deploy_From_Scratch.s.sol --rpc-url http://localhost:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --broadcast --sig "run(string memory configFile)" -- M2_deploy_from_scratch.anvil.config.json 18 | ``` 19 | and finally kill the anvil chain with `Ctrl-C`. Make sure to copy the deployment [output file](https://github.com/Layr-Labs/eigenlayer-contracts/blob/master/script/output/M2_from_scratch_deployment_data.json) to [eigenlayer_deployment_output.json](../../contracts/script/output/31337/eigenlayer_deployment_output.json) so that the tests can find the deployed contracts. 20 | 21 | See the main [README](../../README.md#dependencies) to understand why we deploy from the `experimental-reduce-strategy-manager-bytecode-size` branch of eigenlayer-contracts. -------------------------------------------------------------------------------- /contracts/anvil/start-anvil-chain-with-el-and-avs-deployed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit -o nounset -o pipefail 4 | 5 | # cd to the directory of this script so that this can be run from anywhere 6 | anvil_dir=$( 7 | cd "$(dirname "${BASH_SOURCE[0]}")" 8 | pwd -P 9 | ) 10 | root_dir=$(realpath $anvil_dir/../..) 11 | 12 | set -a 13 | source $anvil_dir/utils.sh 14 | set +a 15 | 16 | # start an anvil instance in the background that has eigenlayer contracts deployed 17 | # we start anvil in the background so that we can run the below script 18 | start_anvil_docker $anvil_dir/contracts-deployed-anvil-state.json "" 19 | 20 | cd $root_dir/contracts 21 | # we need to restart the anvil chain at the correct block, otherwise the indexRegistry has a quorumUpdate at the block number 22 | # at which it was deployed (aka quorum was created/updated), but when we start anvil by loading state file it starts at block number 0 23 | # so calling getOperatorListAtBlockNumber reverts because it thinks there are no quorums registered at block 0 24 | # advancing chain manually like this is a current hack until https://github.com/foundry-rs/foundry/issues/6679 is merged 25 | cast rpc anvil_mine 200 --rpc-url http://localhost:8545 > /dev/null 26 | echo "Anvil is ready. Advanced chain to block-number:" $(cast block-number) 27 | 28 | 29 | docker attach anvil -------------------------------------------------------------------------------- /contracts/anvil/utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o nounset 4 | 5 | parent_path=$( 6 | cd "$(dirname "${BASH_SOURCE[0]}")" 7 | pwd -P 8 | ) 9 | 10 | FOUNDRY_IMAGE=ghcr.io/foundry-rs/foundry:stable 11 | 12 | clean_up() { 13 | # Check if the exit status is non-zero 14 | exit_status=$? 15 | if [ $exit_status -ne 0 ]; then 16 | echo "Script exited due to set -e on line $1 with command '$2'. Exit status: $exit_status" 17 | fi 18 | } 19 | # Use trap to call the clean_up function when the script exits 20 | trap 'clean_up $LINENO "$BASH_COMMAND"' ERR 21 | 22 | # start_anvil_docker $LOAD_STATE_FILE $DUMP_STATE_FILE 23 | start_anvil_docker() { 24 | LOAD_STATE_FILE=$1 25 | DUMP_STATE_FILE=$2 26 | LOAD_STATE_VOLUME_DOCKER_ARG=$([[ -z $LOAD_STATE_FILE ]] && echo "" || echo "-v $LOAD_STATE_FILE:/load-state.json") 27 | DUMP_STATE_VOLUME_DOCKER_ARG=$([[ -z $DUMP_STATE_FILE ]] && echo "" || echo "-v $DUMP_STATE_FILE:/dump-state.json") 28 | LOAD_STATE_ANVIL_ARG=$([[ -z $LOAD_STATE_FILE ]] && echo "" || echo "--load-state /load-state.json") 29 | DUMP_STATE_ANVIL_ARG=$([[ -z $DUMP_STATE_FILE ]] && echo "" || echo "--dump-state /dump-state.json") 30 | 31 | trap 'docker stop anvil 2>/dev/null || true' EXIT 32 | set -o xtrace 33 | docker run --rm -d --name anvil -p 8545:8545 $LOAD_STATE_VOLUME_DOCKER_ARG $DUMP_STATE_VOLUME_DOCKER_ARG \ 34 | --entrypoint anvil \ 35 | $FOUNDRY_IMAGE \ 36 | $LOAD_STATE_ANVIL_ARG $DUMP_STATE_ANVIL_ARG --host 0.0.0.0 37 | set +o xtrace 38 | sleep 2 39 | } 40 | -------------------------------------------------------------------------------- /contracts/config/core/31337.json: -------------------------------------------------------------------------------- 1 | { 2 | "maintainer": "samlaf@eigenlabs.org", 3 | "multisig_addresses": { 4 | "operationsMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 5 | "communityMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 6 | "pauserMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 7 | "executorMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 8 | "timelock": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 9 | }, 10 | "strategies": [ 11 | { 12 | "token_address": "0x0000000000000000000000000000000000000000", 13 | "token_symbol": "WETH", 14 | "max_per_deposit": 115792089237316195423570985008687907853269984665640564039457584007913129639935, 15 | "max_deposits": 115792089237316195423570985008687907853269984665640564039457584007913129639935 16 | } 17 | ], 18 | "allocationManager": { 19 | "init_paused_status": 0, 20 | "DEALLOCATION_DELAY": 900, 21 | "ALLOCATION_CONFIGURATION_DELAY": 1200 22 | }, 23 | "strategyManager": { 24 | "init_paused_status": 0, 25 | "init_withdrawal_delay_blocks": 1 26 | }, 27 | "eigenPod": { 28 | "PARTIAL_WITHDRAWAL_FRAUD_PROOF_PERIOD_BLOCKS": 1, 29 | "MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR": "32000000000" 30 | }, 31 | "eigenPodManager": { 32 | "init_paused_status": 30 33 | }, 34 | "delayedWithdrawalRouter": { 35 | "init_paused_status": 0, 36 | "init_withdrawal_delay_blocks": 1 37 | }, 38 | "slasher": { 39 | "init_paused_status": 0 40 | }, 41 | "delegation": { 42 | "withdrawal_delay_blocks": 900, 43 | "init_paused_status": 0, 44 | "init_withdrawal_delay_blocks": 1 45 | }, 46 | "rewardsCoordinator": { 47 | "init_paused_status": 0, 48 | "CALCULATION_INTERVAL_SECONDS": 604800, 49 | "MAX_REWARDS_DURATION": 6048000, 50 | "MAX_RETROACTIVE_LENGTH": 7776000, 51 | "MAX_FUTURE_LENGTH": 2592000, 52 | "GENESIS_REWARDS_TIMESTAMP": 1710979200, 53 | "rewards_updater_address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 54 | "activation_delay": 7200, 55 | "calculation_interval_seconds": 604800, 56 | "global_operator_commission_bips": 1000, 57 | "OPERATOR_SET_GENESIS_REWARDS_TIMESTAMP": 1720656000, 58 | "OPERATOR_SET_MAX_RETROACTIVE_LENGTH": 2592000 59 | }, 60 | "ethPOSDepositAddress": "0x00000000219ab540356cBB839Cbe05303d7705Fa", 61 | "semver": "v0.0.0" 62 | } 63 | -------------------------------------------------------------------------------- /contracts/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | solc_version = '0.8.27' 6 | via-ir = true 7 | optimizer = true 8 | optimizer_runs = 200 9 | remappings = [ 10 | "@openzeppelin-upgrades-v4.9.0/=lib/eigenlayer-middleware/lib/eigenlayer-contracts/lib/openzeppelin-contracts-upgradeable-v4.9.0/", 11 | "@openzeppelin-upgrades/=lib/eigenlayer-middleware/lib/openzeppelin-contracts-upgradeable/", 12 | "@openzeppelin-v4.9.0/=lib/eigenlayer-middleware/lib/eigenlayer-contracts/lib/openzeppelin-contracts-v4.9.0/", 13 | "@openzeppelin/=lib/eigenlayer-middleware/lib/openzeppelin-contracts/", 14 | "ds-test/=lib/eigenlayer-middleware/lib/ds-test/src/", 15 | "eigenlayer-contracts/=lib/eigenlayer-middleware/lib/eigenlayer-contracts/", 16 | "eigenlayer-middleware/=lib/eigenlayer-middleware/", 17 | "forge-std/=lib/forge-std/src/", 18 | "openzeppelin-contracts-upgradeable/=lib/eigenlayer-middleware/lib/openzeppelin-contracts-upgradeable/", 19 | "openzeppelin-contracts/=lib/eigenlayer-middleware/lib/openzeppelin-contracts/", 20 | ] 21 | fs_permissions = [{ access = "read-write", path = "./" }] 22 | 23 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 24 | -------------------------------------------------------------------------------- /contracts/generate-bindings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit -o nounset -o pipefail 4 | 5 | # cd to the directory of this script so that this can be run from anywhere 6 | script_path=$( 7 | cd "$(dirname "${BASH_SOURCE[0]}")" 8 | pwd -P 9 | ) 10 | 11 | # build abigen-with-interfaces docker image if it doesn't exist 12 | if [[ "$(docker images -q abigen-with-interfaces 2>/dev/null)" == "" ]]; then 13 | docker build -t abigen-with-interfaces -f abigen-with-interfaces.Dockerfile $script_path 14 | fi 15 | 16 | # TODO: refactor this function.. it got really ugly with the dockerizing of abigen 17 | function create_binding { 18 | contract_dir=$1 19 | contract=$2 20 | binding_dir=$3 21 | echo $contract 22 | mkdir -p $binding_dir/${contract} 23 | contract_json="$contract_dir/out/${contract}.sol/${contract}.json" 24 | solc_abi=$(cat ${contract_json} | jq -r '.abi') 25 | solc_bin=$(cat ${contract_json} | jq -r '.bytecode.object') 26 | 27 | mkdir -p data 28 | echo ${solc_abi} >data/tmp.abi 29 | echo ${solc_bin} >data/tmp.bin 30 | 31 | rm -f $binding_dir/${contract}/binding.go 32 | docker run -v $(realpath $binding_dir):/home/binding_dir -v .:/home/repo abigen-with-interfaces --bin=/home/repo/data/tmp.bin --abi=/home/repo/data/tmp.abi --pkg=contract${contract} --out=/home/binding_dir/${contract}/binding.go 33 | rm -rf data/tmp.abi data/tmp.bin 34 | } 35 | 36 | path=$1 37 | echo "Generating bindings for contracts in path: $path" 38 | cd "$path" 39 | pwd 40 | 41 | contracts=$2 42 | bindings_path=$3 43 | 44 | forge build -C src/contracts 45 | echo "Generating bindings for contracts: $contracts" 46 | for contract in $contracts; do 47 | sleep 1 # this is a hack to fix the issue with abigen randomly failing for some contracts 48 | create_binding . "$contract" "$bindings_path" 49 | done -------------------------------------------------------------------------------- /contracts/script/input/31337/ops_addresses.json: -------------------------------------------------------------------------------- 1 | { 2 | "communityMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 3 | "pauser": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 4 | "churner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 5 | "ejector": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 6 | } -------------------------------------------------------------------------------- /contracts/script/output/31337/eigenlayer_deployment_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "addresses": { 3 | "TestToken": "0xc5a5C42992dECbae36851359345FE25997F5C42d", 4 | "allocationManager": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", 5 | "allocationManagerImplementation": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed", 6 | "avsDirectory": "0x0165878A594ca255338adfa4d48449f69242Eb8F", 7 | "avsDirectoryImplementation": "0x0B306BF915C4d645ff596e518fAf3F9669b97016", 8 | "baseStrategyImplementation": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", 9 | "delegationManager": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", 10 | "delegationManagerImplementation": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82", 11 | "eigenLayerPauserReg": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", 12 | "eigenLayerProxyAdmin": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", 13 | "eigenPodBeacon": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", 14 | "eigenPodImplementation": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e", 15 | "eigenPodManager": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", 16 | "eigenPodManagerImplementation": "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1", 17 | "emptyContract": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", 18 | "numStrategiesDeployed": 0, 19 | "permissionController": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", 20 | "permissionControllerImplementation": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c", 21 | "rewardsCoordinator": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", 22 | "rewardsCoordinatorImplementation": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE", 23 | "strategies": { 24 | "WETH": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933" 25 | }, 26 | "strategy": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933", 27 | "strategyManager": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", 28 | "strategyManagerImplementation": "0x9A676e781A523b5d0C0e43731313A708CB607508", 29 | "token": { 30 | "tokenProxyAdmin": "0x0000000000000000000000000000000000000000", 31 | "EIGEN": "0x0000000000000000000000000000000000000000", 32 | "bEIGEN": "0x0000000000000000000000000000000000000000", 33 | "EIGENImpl": "0x0000000000000000000000000000000000000000", 34 | "bEIGENImpl": "0x0000000000000000000000000000000000000000", 35 | "eigenStrategy": "0x0000000000000000000000000000000000000000", 36 | "eigenStrategyImpl": "0x0000000000000000000000000000000000000000" 37 | } 38 | }, 39 | "chainInfo": { 40 | "chainId": 31337, 41 | "deploymentBlock": 1 42 | }, 43 | "parameters": { 44 | "communityMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 45 | "executorMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 46 | "operationsMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 47 | "pauserMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 48 | "timelock": "0x0000000000000000000000000000000000000000" 49 | } 50 | } -------------------------------------------------------------------------------- /contracts/script/output/31337/mockAvs_deployment_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "addresses": { 3 | "mockAvsServiceManager": "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9", 4 | "mockAvsServiceManagerImplementation": "0x0000000000000000000000000000000000000000", 5 | "operatorStateRetriever": "0x0E801D84Fa97b50751Dbf25036d067dCf18858bF", 6 | "proxyAdmin": "0xf5059a5D33d5853360D16C683c16e67980206f36", 7 | "registryCoordinator": "0x95401dc811bb5740090279Ba06cfA8fcF6113778", 8 | "registryCoordinatorImplementation": "0xb7278A61aa25c888815aFC32Ad3cC52fF24fE575" 9 | } 10 | } -------------------------------------------------------------------------------- /contracts/script/output/31337/token_and_strategy_deployment_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "addresses": { 3 | "erc20mock": "0x51A1ceB83B83F1985a81C295d1fF28Afef186E02", 4 | "erc20mockstrategy": "0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7" 5 | } 6 | } -------------------------------------------------------------------------------- /contracts/script/parsers/ConfigsReadWriter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | import "eigenlayer-middleware/src/interfaces/IRegistryCoordinator.sol"; 5 | import "eigenlayer-contracts/src/contracts/strategies/StrategyBase.sol"; 6 | 7 | import "forge-std/Script.sol"; 8 | import "forge-std/StdJson.sol"; 9 | 10 | contract ConfigsReadWriter is Script { 11 | // Forge scripts best practice: https://book.getfoundry.sh/tutorials/best-practices#scripts 12 | // inputFileName should not contain the .json extension, we add it automatically 13 | function readInput(string memory inputFileName) internal view returns (string memory) { 14 | string memory inputDir = string.concat(vm.projectRoot(), "/script/input/"); 15 | string memory chainDir = string.concat(vm.toString(block.chainid), "/"); 16 | string memory file = string.concat(inputFileName, ".json"); 17 | return vm.readFile(string.concat(inputDir, chainDir, file)); 18 | } 19 | 20 | function readOutput(string memory outputFileName) internal view returns (string memory) { 21 | string memory inputDir = string.concat(vm.projectRoot(), "/script/output/"); 22 | string memory chainDir = string.concat(vm.toString(block.chainid), "/"); 23 | string memory file = string.concat(outputFileName, ".json"); 24 | return vm.readFile(string.concat(inputDir, chainDir, file)); 25 | } 26 | 27 | function writeOutput(string memory outputJson, string memory outputFileName) internal { 28 | string memory outputDir = string.concat(vm.projectRoot(), "/script/output/"); 29 | string memory chainDir = string.concat(vm.toString(block.chainid), "/"); 30 | string memory outputFilePath = string.concat(outputDir, chainDir, outputFileName, ".json"); 31 | vm.writeJson(outputJson, outputFilePath); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/script/parsers/MockAvsContractsParser.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.27; 3 | 4 | import {SlashingRegistryCoordinator} from "eigenlayer-middleware/src/SlashingRegistryCoordinator.sol"; 5 | import {OperatorStateRetriever} from "eigenlayer-middleware/src/OperatorStateRetriever.sol"; 6 | 7 | import {MockAvsServiceManager} from "../../src/MockAvsServiceManager.sol"; 8 | 9 | import {ConfigsReadWriter} from "./ConfigsReadWriter.sol"; 10 | import "forge-std/StdJson.sol"; 11 | 12 | struct MockAvsContracts { 13 | MockAvsServiceManager mockAvsServiceManager; 14 | SlashingRegistryCoordinator registryCoordinator; 15 | OperatorStateRetriever operatorStateRetriever; 16 | } 17 | 18 | contract MockAvsContractsParser is ConfigsReadWriter { 19 | function _loadMockAvsDeployedContracts() internal view returns (MockAvsContracts memory) { 20 | // Eigenlayer contracts 21 | string memory mockAvsDeployedContracts = readOutput("mockavs_deployment_output"); 22 | MockAvsServiceManager mockAvsServiceManager = 23 | MockAvsServiceManager(stdJson.readAddress(mockAvsDeployedContracts, ".addresses.mockAvsServiceManager")); 24 | require( 25 | address(mockAvsServiceManager) != address(0), "MockAvsContractsParser: mockAvsServiceManager address is 0" 26 | ); 27 | SlashingRegistryCoordinator registryCoordinator = 28 | SlashingRegistryCoordinator(stdJson.readAddress(mockAvsDeployedContracts, ".addresses.registryCoordinator")); 29 | require(address(registryCoordinator) != address(0), "MockAvsContractsParser: registryCoordinator address is 0"); 30 | OperatorStateRetriever operatorStateRetriever = 31 | OperatorStateRetriever(stdJson.readAddress(mockAvsDeployedContracts, ".addresses.operatorStateRetriever")); 32 | require( 33 | address(operatorStateRetriever) != address(0), "MockAvsContractsParser: operatorStateRetriever address is 0" 34 | ); 35 | 36 | return MockAvsContracts(mockAvsServiceManager, registryCoordinator, operatorStateRetriever); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /contracts/script/parsers/TokensAndStrategiesContractsParser.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.27; 3 | 4 | import "@openzeppelin/contracts/interfaces/IERC20.sol"; 5 | import {IStrategyManager, IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; 6 | 7 | import {ConfigsReadWriter} from "./ConfigsReadWriter.sol"; 8 | import "forge-std/StdJson.sol"; 9 | 10 | struct TokenAndStrategyContracts { 11 | IERC20 token; 12 | IStrategy strategy; 13 | } 14 | 15 | // TODO: support more than one token/strategy pair (dee deployTokensAndStrategies script) 16 | contract TokenAndStrategyContractsParser is ConfigsReadWriter { 17 | function _loadTokenAndStrategyContracts() internal view returns (TokenAndStrategyContracts memory) { 18 | // Token and Strategy contracts 19 | string memory tokenAndStrategyConfigFile = readOutput("token_and_strategy_deployment_output"); 20 | 21 | bytes memory tokensAndStrategiesConfigsRaw = stdJson.parseRaw(tokenAndStrategyConfigFile, ".addresses"); 22 | TokenAndStrategyContracts memory tokensAndStrategiesContracts = 23 | abi.decode(tokensAndStrategiesConfigsRaw, (TokenAndStrategyContracts)); 24 | 25 | return tokensAndStrategiesContracts; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /contracts/src/ContractsRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.27; 3 | 4 | // ContractsRegistry store the address of all the contracts deployed for eigenlayer and avss 5 | // It is used for testing purpose only, so that we can retrieve the addresses without having to store them in a json file 6 | // This way integration testing against an anvil chain (started with a saved db) is self-contained 7 | // ASSUMPTION: right now we deploy this contract as the first deployment (nonce=0) using the first anvil address 8 | // 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 to make sure it's always at the address 0x5FbDB2315678afecb367f032d93F642f64180aa3 9 | // forge create src/ContractsRegistry.sol:ContractsRegistry --rpc-url $RPC_URL --private-key $PRIVATE_KEY 10 | contract ContractsRegistry { 11 | mapping(string => address) public contracts; 12 | mapping(uint256 => string) public contractNames; 13 | uint256 public contractCount; 14 | 15 | function registerContract(string memory name, address _contract) public { 16 | // we treat redeploys as a bug since this is only meant to be used for testing. 17 | // If new contracts need to be deployed just start from a fresh anvil state. 18 | require(contracts[name] == address(0), "contract already registered"); 19 | contracts[name] = _contract; 20 | contractNames[contractCount] = name; 21 | contractCount++; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/src/MockAvsServiceManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.27; 3 | 4 | import {IAVSDirectory} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; 5 | import {IRewardsCoordinator} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol"; 6 | import {IAllocationManager} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; 7 | import {IPermissionController} from "eigenlayer-contracts/src/contracts/interfaces/IPermissionController.sol"; 8 | 9 | import {ISlashingRegistryCoordinator} from "eigenlayer-middleware/src/interfaces/ISlashingRegistryCoordinator.sol"; 10 | import {IBLSSignatureChecker} from "eigenlayer-middleware/src/interfaces/IBLSSignatureChecker.sol"; 11 | import {ServiceManagerBase} from "eigenlayer-middleware/src/ServiceManagerBase.sol"; 12 | import {BLSSignatureChecker} from "eigenlayer-middleware/src/BLSSignatureChecker.sol"; 13 | 14 | contract MockAvsServiceManager is ServiceManagerBase, BLSSignatureChecker { 15 | constructor( 16 | ISlashingRegistryCoordinator _registryCoordinator, 17 | IAVSDirectory _avsDirectory, 18 | IRewardsCoordinator _rewardsCoordinator, 19 | IPermissionController _permissionController, 20 | IAllocationManager _allocationManager 21 | ) 22 | ServiceManagerBase( 23 | _avsDirectory, 24 | _rewardsCoordinator, 25 | _registryCoordinator, 26 | _registryCoordinator.stakeRegistry(), 27 | _permissionController, 28 | _allocationManager 29 | ) 30 | BLSSignatureChecker(_registryCoordinator) 31 | {} 32 | 33 | function initialize(address _initialOwner) external initializer { 34 | // TODO: setting _rewardsInitiator to be _initialOwner for now. 35 | __ServiceManagerBase_init(_initialOwner, _initialOwner); 36 | _permissionController.addPendingAdmin(address(this), _initialOwner); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /contracts/src/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.27; 3 | 4 | import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract MockERC20 is ERC20("Mock Token", "MCK") { 7 | function mint(address account, uint256 amount) public { 8 | _mint(account, amount); 9 | } 10 | 11 | /// WARNING: Vulnerable, bypasses allowance check. Do not use in production! 12 | function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) { 13 | super._transfer(from, to, amount); 14 | return true; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crypto/bls/attestation_test.go: -------------------------------------------------------------------------------- 1 | package bls 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestBlsKeyCreation(t *testing.T) { 12 | var tests = map[string]struct { 13 | keyPath string 14 | password string 15 | wantErr bool 16 | }{ 17 | "valid bls key save": { 18 | keyPath: "./operator_keys_test_directory/test.bls.key.json", 19 | password: "test", 20 | wantErr: false, 21 | }, 22 | } 23 | 24 | for name, tt := range tests { 25 | t.Run(name, func(t *testing.T) { 26 | t.Cleanup(func() { 27 | dir := filepath.Dir(tt.keyPath) 28 | _ = os.RemoveAll(dir) 29 | }) 30 | randomKey, err := GenRandomBlsKeys() 31 | assert.NoError(t, err) 32 | 33 | err = randomKey.SaveToFile(tt.keyPath, tt.password) 34 | if tt.wantErr { 35 | assert.Error(t, err) 36 | } else { 37 | assert.NoError(t, err) 38 | } 39 | 40 | readKeyPair, err := ReadPrivateKeyFromFile(tt.keyPath, tt.password) 41 | if tt.wantErr { 42 | assert.Error(t, err) 43 | } else { 44 | assert.NoError(t, err) 45 | assert.Equal(t, randomKey, readKeyPair) 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crypto/ecdsa/utils_test.go: -------------------------------------------------------------------------------- 1 | package ecdsa 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/ethereum/go-ethereum/crypto" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestECDSAPrivateKey(t *testing.T) { 13 | var tests = map[string]struct { 14 | keyPath string 15 | password string 16 | wantErr bool 17 | }{ 18 | "valid ecdsa key save": { 19 | keyPath: "./operator_keys_test_directory/test.ecdsa.key.json", 20 | password: "test", 21 | wantErr: false, 22 | }, 23 | } 24 | 25 | for name, tt := range tests { 26 | t.Run(name, func(t *testing.T) { 27 | t.Cleanup(func() { 28 | dir := filepath.Dir(tt.keyPath) 29 | _ = os.RemoveAll(dir) 30 | }) 31 | randomKey, err := crypto.GenerateKey() 32 | assert.NoError(t, err) 33 | 34 | err = WriteKey(tt.keyPath, randomKey, tt.password) 35 | if tt.wantErr { 36 | assert.Error(t, err) 37 | } else { 38 | assert.NoError(t, err) 39 | } 40 | 41 | readKeyPair, err := ReadKey(tt.keyPath, tt.password) 42 | if tt.wantErr { 43 | assert.Error(t, err) 44 | } else { 45 | assert.NoError(t, err) 46 | assert.Equal(t, randomKey, readKeyPair) 47 | } 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crypto/utils/batch_key.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/Layr-Labs/eigensdk-go/utils" 10 | ) 11 | 12 | const ( 13 | // TODO: move to a common constants file which 14 | // can be used by utils and cmd 15 | DefaultKeyFolder = "keys" 16 | PasswordFile = "password.txt" 17 | PrivateKeyHexFile = "private_key_hex.txt" 18 | ) 19 | 20 | // ReadBatchKeys reads the batch keys from the given folder 21 | // and returns the list of BatchKey 22 | // folder: folder where the keys are stored, relative to the current directory 23 | func ReadBatchKeys(folder string, isECDSA bool) ([]BatchKey, error) { 24 | var batchKey []BatchKey 25 | absFolder, err := filepath.Abs(folder) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | // read the private key file 31 | privateKeyFile, err := os.Open(filepath.Clean(absFolder + "/" + PrivateKeyHexFile)) 32 | if err != nil { 33 | fmt.Println("Error opening the file:", err) 34 | return nil, err 35 | } 36 | defer func(privateKeyFile *os.File) { 37 | err := privateKeyFile.Close() 38 | if err != nil { 39 | _ = utils.WrapError("error closing the file", err) 40 | return 41 | } 42 | }(privateKeyFile) 43 | 44 | // read the password file 45 | passwordFile, err := os.Open(filepath.Clean(absFolder + "/" + PasswordFile)) 46 | if err != nil { 47 | fmt.Println("Error opening the file:", err) 48 | return nil, utils.WrapError("error opening the file", err) 49 | } 50 | defer func(passwordFile *os.File) { 51 | err := passwordFile.Close() 52 | if err != nil { 53 | _ = utils.WrapError("error closing the file", err) 54 | return 55 | } 56 | }(passwordFile) 57 | 58 | privateKeyScanner := bufio.NewScanner(privateKeyFile) 59 | passwordScanner := bufio.NewScanner(passwordFile) 60 | 61 | // To create file names 62 | fileCtr := 1 63 | keyType := "ecdsa" 64 | if !isECDSA { 65 | keyType = "bls" 66 | } 67 | for privateKeyScanner.Scan() && passwordScanner.Scan() { 68 | privateKey := privateKeyScanner.Text() 69 | password := passwordScanner.Text() 70 | fileName := fmt.Sprintf("%d.%s.key.json", fileCtr, keyType) 71 | filePath := fmt.Sprintf("%s/%s/%s", absFolder, DefaultKeyFolder, fileName) 72 | // Since a last line with empty string is also read 73 | // I need to break here 74 | // TODO(madhur): remove empty string when it gets created 75 | if privateKey == "" { 76 | break 77 | } 78 | 79 | bK := BatchKey{ 80 | FilePath: filePath, 81 | Password: password, 82 | PrivateKey: privateKey, 83 | } 84 | batchKey = append(batchKey, bK) 85 | fileCtr++ 86 | } 87 | 88 | if err := privateKeyScanner.Err(); err != nil { 89 | fmt.Println("Error reading privateKey file: ", err) 90 | return nil, err 91 | } 92 | 93 | if err := passwordScanner.Err(); err != nil { 94 | fmt.Println("Error reading password file: ", err) 95 | return nil, err 96 | } 97 | 98 | return batchKey, nil 99 | } 100 | -------------------------------------------------------------------------------- /crypto/utils/batch_key_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/Layr-Labs/eigensdk-go/crypto/bls" 9 | "github.com/Layr-Labs/eigensdk-go/crypto/ecdsa" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestReadBatchKeys(t *testing.T) { 14 | 15 | var tests = []struct { 16 | keyFolder string 17 | isEcdsa bool 18 | }{ 19 | { 20 | keyFolder: "test_data/bls_keys", 21 | isEcdsa: false, 22 | }, 23 | { 24 | keyFolder: "test_data/ecdsa_keys", 25 | isEcdsa: true, 26 | }, 27 | } 28 | 29 | for _, test := range tests { 30 | readKeys, err := ReadBatchKeys(test.keyFolder, test.isEcdsa) 31 | if err != nil { 32 | t.Errorf("Error reading batch keys: %s", err) 33 | } 34 | 35 | if test.isEcdsa { 36 | for _, key := range readKeys { 37 | fmt.Printf("Valdiate ecdsa key: %s with password %s\n", key.FilePath, key.Password) 38 | pk, err := ecdsa.ReadKey(key.FilePath, key.Password) 39 | if err != nil { 40 | assert.Fail(t, "Error reading ecdsa key") 41 | break 42 | } 43 | assert.Equal(t, key.PrivateKey, "0x"+hex.EncodeToString(pk.D.Bytes())) 44 | } 45 | } else { 46 | for _, key := range readKeys { 47 | fmt.Printf("Valdiate bls key: %s with password %s\n", key.FilePath, key.Password) 48 | pk, err := bls.ReadPrivateKeyFromFile(key.FilePath, key.Password) 49 | if err != nil { 50 | assert.Fail(t, "Error reading bls key") 51 | break 52 | } 53 | assert.Equal(t, key.PrivateKey, pk.PrivKey.String()) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /crypto/utils/test_data/bls_keys/keys/1.bls.key.json: -------------------------------------------------------------------------------- 1 | {"pubKey":"E([19408553463882111916887171276012224475029133183214861480489485386352635269635,17418827901203159022109906145273000034647571131322064812191371351028964064220])","crypto":{"cipher":"aes-128-ctr","ciphertext":"f2e5a3df524234426297f84b80c3c69e008ca17960fa512845b250a5308a7a6c","cipherparams":{"iv":"64528130796bc7227f48b0b01030b4c2"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"7ceca78a0011e3ab5dce8b4f562b0615f60d8642b841d751bc676fb7dc574938"},"mac":"f9e5050e1077e2558a66f82fad05259ebc9e81da6e5ef8025f34d8fbc7e6e8ac"}} -------------------------------------------------------------------------------- /crypto/utils/test_data/bls_keys/keys/2.bls.key.json: -------------------------------------------------------------------------------- 1 | {"pubKey":"E([1611472477336575391907540595283981736749839435478357492584206660415845982634,17740534282163859696734712865013083642718796435843138137894885755851743300823])","crypto":{"cipher":"aes-128-ctr","ciphertext":"d6dd67cddc77447c63b3bba2a1ffb115af151a31c3a9811b40b7b3b965799402","cipherparams":{"iv":"bf935b67d026db7f0502a46b8e35bddd"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"85c1c9b32e1182b5d769cce7005b00f5c64eec2537636e8b2a3d125083bc8ef6"},"mac":"b398381ce566074af656237d71be1551254f8824221690b59dd9bcdbcb409055"}} -------------------------------------------------------------------------------- /crypto/utils/test_data/bls_keys/keys/3.bls.key.json: -------------------------------------------------------------------------------- 1 | {"pubKey":"E([3336192159512049190945679273141887248666932624338963482128432381981287252980,15195175002875833468883745675063986308012687914999552116603423331534089122704])","crypto":{"cipher":"aes-128-ctr","ciphertext":"c7fc50d7cd8e324f8e27d4f14991dc821b6552496c5c0330506d239bd11f5fca","cipherparams":{"iv":"e9a0897abbd105b322ecaecee5acb344"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"1bb93fdaef6fe17cb50d1d7963f27efa025e09390b4718e56f51479b85937d78"},"mac":"4c28491d98ee0181d99277f011c2a396dbd29d8b6461fb3d802a0ba4317955ba"}} -------------------------------------------------------------------------------- /crypto/utils/test_data/bls_keys/password.txt: -------------------------------------------------------------------------------- 1 | fDUMDLmBROwlzzPXyIcy 2 | 2EVEUyHCrHZdfdo8lp29 3 | k1ZxvbBylq0lscHnrrJy 4 | -------------------------------------------------------------------------------- /crypto/utils/test_data/bls_keys/private_key_hex.txt: -------------------------------------------------------------------------------- 1 | 2215338531151182997276243965065522514190247674553811942190946030173209230351 2 | 5217984197168966461576865353015567761629607981429081178519583306084941850805 3 | 16834990251706844646759019708813363710810183547292596296141001406129498851847 4 | -------------------------------------------------------------------------------- /crypto/utils/test_data/ecdsa_keys/keys/1.ecdsa.key.json: -------------------------------------------------------------------------------- 1 | {"address":"d5a0359da7b310917d7760385516b2426e86ab7f","crypto":{"cipher":"aes-128-ctr","ciphertext":"91a55f690a65c9b352a24783d1db851a7b2f826763ff979a877bb78ae63860eb","cipherparams":{"iv":"313c5db87ccef736f2844c218b0728c2"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"64e28acc8270b64aa22fb6554a694657face2887ceb6cc209c607db11c4338d7"},"mac":"9087dd2b17322a7237505b368eac0462dd8c87637516cd80740fafe7d2f3ae5b"},"id":"96c2a806-cd73-4e95-8d65-e0933a3e7e1c","version":3} -------------------------------------------------------------------------------- /crypto/utils/test_data/ecdsa_keys/keys/2.ecdsa.key.json: -------------------------------------------------------------------------------- 1 | {"address":"9441540e8183d416f2dc1901ab2034600f17b65a","crypto":{"cipher":"aes-128-ctr","ciphertext":"f5eb7776cb577b05ac7762177e447f15c1adcfab57a39f757f3e4b92af6f661b","cipherparams":{"iv":"2b41e5cbb0bc3ca4c380071831f00ae4"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"e984d8a762013fd76e5a7c6df30de81a613899962a4c154f370259274b7e6b83"},"mac":"54b650442a628bdce4585d499f5bdf4e35c7cee81b90cdcbd89db4646f595aa2"},"id":"e8555893-d21a-48d7-98a6-06b7f07eba39","version":3} -------------------------------------------------------------------------------- /crypto/utils/test_data/ecdsa_keys/keys/3.ecdsa.key.json: -------------------------------------------------------------------------------- 1 | {"address":"f270e0dd5a1cc34cda8fb991307604097e622002","crypto":{"cipher":"aes-128-ctr","ciphertext":"d382b45ad3a9b69fd5933a38d711996b5283bb0e6a71e6da09edeb6c4a23b069","cipherparams":{"iv":"cf764c9c72afcf5748131711f61c49d0"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"bbdbccf9058c71c63cd251a9d975454ecdbd5f7795329954867a607512492b0d"},"mac":"3c16f6528a7812c2da48ff2d251e4c830875f5fcff6694a81b908cabdb809a7c"},"id":"05b7b538-57c6-489c-bf34-fe25409e7bf4","version":3} -------------------------------------------------------------------------------- /crypto/utils/test_data/ecdsa_keys/password.txt: -------------------------------------------------------------------------------- 1 | EnJuncq01CiVk9UbuBYl 2 | isru1gvtykIavuk1Fg1Q 3 | 3bxTdXda0Kwvo8KC9GGT 4 | -------------------------------------------------------------------------------- /crypto/utils/test_data/ecdsa_keys/private_key_hex.txt: -------------------------------------------------------------------------------- 1 | 0xd1d51de8ce6bbaac0572e481268232898bfe46491766214c5738929dd557c552 2 | 0x6374444d520f8ae51eee2683f4790644ee5f2d95ca4382fa78021e0460cb1663 3 | 0xa2788f1c26c799b7e1ac32ababc0b598fc7e9c6fc3d319c461ae67ffb1ee57dd 4 | -------------------------------------------------------------------------------- /crypto/utils/types.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type BatchKey struct { 4 | PrivateKey string 5 | FilePath string 6 | Password string 7 | } 8 | -------------------------------------------------------------------------------- /internal/fakes/avs_registry.go: -------------------------------------------------------------------------------- 1 | package fakes 2 | 3 | import ( 4 | "context" 5 | "math/big" 6 | 7 | apkregistrybindings "github.com/Layr-Labs/eigensdk-go/contracts/bindings/BLSApkRegistry" 8 | opstateretriever "github.com/Layr-Labs/eigensdk-go/contracts/bindings/OperatorStateRetriever" 9 | "github.com/Layr-Labs/eigensdk-go/types" 10 | 11 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 12 | "github.com/ethereum/go-ethereum/common" 13 | ) 14 | 15 | type TestOperator struct { 16 | OperatorAddr common.Address 17 | OperatorInfo types.OperatorInfo 18 | ContractG1Pubkey apkregistrybindings.BN254G1Point 19 | ContractG2Pubkey apkregistrybindings.BN254G2Point 20 | OperatorId types.OperatorId 21 | } 22 | 23 | type FakeAVSRegistryReader struct { 24 | opAddress []types.OperatorAddr 25 | opPubKeys []types.OperatorPubkeys 26 | operatorId types.OperatorId 27 | socket types.Socket 28 | err error 29 | } 30 | 31 | func NewFakeAVSRegistryReader( 32 | opr *TestOperator, 33 | err error, 34 | ) *FakeAVSRegistryReader { 35 | if opr == nil { 36 | return &FakeAVSRegistryReader{} 37 | } 38 | return &FakeAVSRegistryReader{ 39 | opAddress: []common.Address{opr.OperatorAddr}, 40 | opPubKeys: []types.OperatorPubkeys{opr.OperatorInfo.Pubkeys}, 41 | socket: opr.OperatorInfo.Socket, 42 | operatorId: opr.OperatorId, 43 | err: err, 44 | } 45 | } 46 | 47 | func (f *FakeAVSRegistryReader) QueryExistingRegisteredOperatorPubKeys( 48 | ctx context.Context, 49 | startBlock *big.Int, 50 | stopBlock *big.Int, 51 | blockRange *big.Int, 52 | ) ([]types.OperatorAddr, []types.OperatorPubkeys, error) { 53 | return f.opAddress, f.opPubKeys, f.err 54 | } 55 | 56 | func (f *FakeAVSRegistryReader) QueryExistingRegisteredOperatorSockets( 57 | ctx context.Context, 58 | startBlock *big.Int, 59 | stopBlock *big.Int, 60 | blockRange *big.Int, 61 | ) (map[types.OperatorId]types.Socket, error) { 62 | if len(f.opPubKeys) == 0 { 63 | return nil, nil 64 | } 65 | 66 | return map[types.OperatorId]types.Socket{ 67 | types.OperatorIdFromG1Pubkey(f.opPubKeys[0].G1Pubkey): f.socket, 68 | }, nil 69 | } 70 | 71 | func (f *FakeAVSRegistryReader) GetOperatorFromId( 72 | opts *bind.CallOpts, 73 | operatorId types.OperatorId, 74 | ) (common.Address, error) { 75 | return f.opAddress[0], f.err 76 | } 77 | 78 | func (f *FakeAVSRegistryReader) GetOperatorsStakeInQuorumsAtBlock( 79 | opts *bind.CallOpts, 80 | quorumNumbers types.QuorumNums, 81 | blockNumber types.BlockNum, 82 | ) ([][]opstateretriever.OperatorStateRetrieverOperator, error) { 83 | return [][]opstateretriever.OperatorStateRetrieverOperator{ 84 | { 85 | { 86 | OperatorId: f.operatorId, 87 | Stake: big.NewInt(123), 88 | }, 89 | }, 90 | }, nil 91 | } 92 | 93 | func (f *FakeAVSRegistryReader) GetCheckSignaturesIndices( 94 | opts *bind.CallOpts, 95 | referenceBlockNumber uint32, 96 | quorumNumbers types.QuorumNums, 97 | nonSignerOperatorIds []types.OperatorId, 98 | ) (opstateretriever.OperatorStateRetrieverCheckSignaturesIndices, error) { 99 | return opstateretriever.OperatorStateRetrieverCheckSignaturesIndices{}, nil 100 | } 101 | -------------------------------------------------------------------------------- /internal/fakes/eth_client.go: -------------------------------------------------------------------------------- 1 | package fakes 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "math/big" 7 | 8 | "github.com/ethereum/go-ethereum" 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/ethereum/go-ethereum/core/types" 11 | ) 12 | 13 | const ( 14 | TransactionHash = "0x0000000000000000000000000000000000000000000000000000000000001234" 15 | TransactionNashNotInFake = "0xabcd" 16 | BlockNumber = 1234 17 | ) 18 | 19 | type EthClient struct { 20 | chainId *big.Int 21 | successfulTxs map[common.Hash]bool 22 | } 23 | 24 | // NewEthClient returns a new instance of the EthClient 25 | // Right now this client is hardcoded with some values to satisfy current 26 | // testing requirements, but it can be extended to support more features and 27 | // can be made more generic over time when we add more tests. 28 | // Currently used in 29 | // - chainio/clients/wallet/fireblocks_wallet_test.go 30 | func NewEthClient() *EthClient { 31 | return &EthClient{ 32 | chainId: big.NewInt(5), 33 | successfulTxs: map[common.Hash]bool{ 34 | common.HexToHash(TransactionHash): true, 35 | }, 36 | } 37 | } 38 | 39 | func (f *EthClient) ChainID(ctx context.Context) (*big.Int, error) { 40 | return f.chainId, nil 41 | } 42 | 43 | func (f *EthClient) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { 44 | if _, ok := f.successfulTxs[txHash]; !ok { 45 | return nil, errors.New("tx not found") 46 | } 47 | 48 | return &types.Receipt{ 49 | TxHash: txHash, 50 | BlockNumber: big.NewInt(BlockNumber), 51 | }, nil 52 | } 53 | 54 | func (f *EthClient) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { 55 | return []byte{}, nil 56 | } 57 | 58 | func (f *EthClient) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { 59 | return []byte{}, nil 60 | } 61 | 62 | func (f *EthClient) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { 63 | return 0, nil 64 | } 65 | 66 | func (f *EthClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) { 67 | return big.NewInt(0), nil 68 | } 69 | 70 | func (f *EthClient) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { 71 | return big.NewInt(0), nil 72 | } 73 | 74 | func (f *EthClient) SendTransaction(ctx context.Context, tx *types.Transaction) error { 75 | return nil 76 | } 77 | 78 | func (f *EthClient) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { 79 | return &types.Header{}, nil 80 | } 81 | 82 | func (f *EthClient) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { 83 | return []byte{}, nil 84 | } 85 | 86 | func (f *EthClient) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { 87 | return 0, nil 88 | } 89 | 90 | func (f *EthClient) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { 91 | return []types.Log{}, nil 92 | } 93 | 94 | func (f *EthClient) SubscribeFilterLogs( 95 | ctx context.Context, 96 | q ethereum.FilterQuery, 97 | ch chan<- types.Log, 98 | ) (ethereum.Subscription, error) { 99 | return nil, nil 100 | } 101 | -------------------------------------------------------------------------------- /logging/README.md: -------------------------------------------------------------------------------- 1 | ## Logging 2 | We use Zap logger for this module 3 | 4 | ``` 5 | logger, err := NewZapLogger(env) 6 | ``` -------------------------------------------------------------------------------- /logging/constants.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | const ( 4 | ComponentKey = "component" 5 | ) 6 | -------------------------------------------------------------------------------- /logging/logger.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | type Logger interface { 4 | Debug(msg string, tags ...any) 5 | 6 | Info(msg string, tags ...any) 7 | 8 | Warn(msg string, tags ...any) 9 | 10 | Error(msg string, tags ...any) 11 | 12 | Fatal(msg string, tags ...any) 13 | 14 | Debugf(template string, args ...interface{}) 15 | 16 | Infof(template string, args ...interface{}) 17 | 18 | Warnf(template string, args ...interface{}) 19 | 20 | Errorf(template string, args ...interface{}) 21 | 22 | Fatalf(template string, args ...interface{}) 23 | 24 | With(tags ...any) Logger 25 | } 26 | -------------------------------------------------------------------------------- /logging/zap_logger.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Layr-Labs/eigensdk-go/utils" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | type LogLevel string 11 | 12 | const ( 13 | Development LogLevel = "development" // prints debug and above 14 | Production LogLevel = "production" // prints info and above 15 | ) 16 | 17 | type ZapLogger struct { 18 | logger *zap.Logger 19 | } 20 | 21 | var _ Logger = (*ZapLogger)(nil) 22 | 23 | // NewZapLogger creates a new logger wrapped the zap.Logger 24 | func NewZapLogger(env LogLevel) (Logger, error) { 25 | var config zap.Config 26 | 27 | if env == Production { 28 | config = zap.NewProductionConfig() 29 | } else if env == Development { 30 | config = zap.NewDevelopmentConfig() 31 | } else { 32 | return nil, fmt.Errorf("unknown environment. Expected %s or %s. Received %s", Development, Production, env) 33 | } 34 | 35 | return NewZapLoggerByConfig(config, zap.AddCallerSkip(1)) 36 | } 37 | 38 | // NewZapLoggerByConfig creates a logger wrapped the zap.Logger 39 | // Note if the logger need to show the caller, need use `zap.AddCallerSkip(1)` ad options 40 | func NewZapLoggerByConfig(config zap.Config, options ...zap.Option) (Logger, error) { 41 | logger, err := config.Build(options...) 42 | if err != nil { 43 | return nil, utils.WrapError("Can not build config with the given options", err) 44 | } 45 | 46 | return &ZapLogger{ 47 | logger: logger, 48 | }, nil 49 | } 50 | 51 | func (z *ZapLogger) Debug(msg string, tags ...any) { 52 | z.logger.Sugar().Debugw(msg, tags...) 53 | } 54 | 55 | func (z *ZapLogger) Info(msg string, tags ...any) { 56 | z.logger.Sugar().Infow(msg, tags...) 57 | } 58 | 59 | func (z *ZapLogger) Warn(msg string, tags ...any) { 60 | z.logger.Sugar().Warnw(msg, tags...) 61 | } 62 | 63 | func (z *ZapLogger) Error(msg string, tags ...any) { 64 | z.logger.Sugar().Errorw(msg, tags...) 65 | } 66 | 67 | func (z *ZapLogger) Fatal(msg string, tags ...any) { 68 | z.logger.Sugar().Fatalw(msg, tags...) 69 | } 70 | 71 | func (z *ZapLogger) Debugf(template string, args ...interface{}) { 72 | z.logger.Sugar().Debugf(template, args...) 73 | } 74 | 75 | func (z *ZapLogger) Infof(template string, args ...interface{}) { 76 | z.logger.Sugar().Infof(template, args...) 77 | } 78 | 79 | func (z *ZapLogger) Warnf(template string, args ...interface{}) { 80 | z.logger.Sugar().Warnf(template, args...) 81 | } 82 | 83 | func (z *ZapLogger) Errorf(template string, args ...interface{}) { 84 | z.logger.Sugar().Errorf(template, args...) 85 | } 86 | 87 | func (z *ZapLogger) Fatalf(template string, args ...interface{}) { 88 | z.logger.Sugar().Fatalf(template, args...) 89 | } 90 | 91 | func (z *ZapLogger) With(tags ...any) Logger { 92 | return &ZapLogger{ 93 | logger: z.logger.Sugar().With(tags...).Desugar(), 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /metrics/README.md: -------------------------------------------------------------------------------- 1 | # Eigen Metrics 2 | 3 | This package implements the eigenlayer avs node spec [eigen metrics](https://docs.eigenlayer.xyz/category/metrics). 4 | 5 | For an example of how to use this package, see [eigen-metrics-example](./eigenmetrics_example_test.go) -------------------------------------------------------------------------------- /metrics/collectors/economic/economic_test.go: -------------------------------------------------------------------------------- 1 | package economic 2 | 3 | import ( 4 | "context" 5 | "math/big" 6 | "testing" 7 | 8 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 9 | "github.com/ethereum/go-ethereum/common" 10 | 11 | "github.com/prometheus/client_golang/prometheus/testutil" 12 | "github.com/stretchr/testify/assert" 13 | "go.uber.org/mock/gomock" 14 | 15 | "github.com/Layr-Labs/eigensdk-go/testutils" 16 | "github.com/Layr-Labs/eigensdk-go/types" 17 | ) 18 | 19 | const registeredOpAddress = "0xb81b18c988bfc7d131fca985a9c531f325e98a2f" 20 | 21 | type fakeELReader struct { 22 | registeredOperators map[common.Address]bool 23 | } 24 | 25 | func newFakeELReader() *fakeELReader { 26 | registeredOperators := make(map[common.Address]bool) 27 | registeredOperators[common.HexToAddress(registeredOpAddress)] = false 28 | return &fakeELReader{ 29 | registeredOperators: registeredOperators, 30 | } 31 | } 32 | 33 | func (f *fakeELReader) OperatorIsFrozen(ctx context.Context, operatorAddr common.Address) (bool, error) { 34 | return f.registeredOperators[operatorAddr], nil 35 | } 36 | 37 | type fakeAVSRegistryReader struct { 38 | operatorId types.OperatorId 39 | stakes map[types.QuorumNum]*big.Int 40 | } 41 | 42 | func (f *fakeAVSRegistryReader) GetOperatorId(opts *bind.CallOpts, operatorAddr common.Address) ([32]byte, error) { 43 | return f.operatorId, nil 44 | } 45 | 46 | func (f *fakeAVSRegistryReader) GetOperatorStakeInQuorumsOfOperatorAtCurrentBlock( 47 | opts *bind.CallOpts, 48 | operatorId types.OperatorId, 49 | ) (map[types.QuorumNum]*big.Int, error) { 50 | return f.stakes, nil 51 | } 52 | 53 | func newFakeAVSRegistryReader() *fakeAVSRegistryReader { 54 | operatorId := types.OperatorId{1} 55 | stakes := map[types.QuorumNum]*big.Int{ 56 | 0: big.NewInt(1000), 57 | 1: big.NewInt(2000), 58 | } 59 | return &fakeAVSRegistryReader{ 60 | operatorId: operatorId, 61 | stakes: stakes, 62 | } 63 | } 64 | 65 | func TestEconomicCollector(t *testing.T) { 66 | operatorAddr := common.HexToAddress(registeredOpAddress) 67 | quorumNames := map[types.QuorumNum]string{ 68 | 0: "ethQuorum", 69 | 1: "someOtherTokenQuorum", 70 | } 71 | 72 | mockCtrl := gomock.NewController(t) 73 | defer mockCtrl.Finish() 74 | 75 | elReader := newFakeELReader() 76 | avsRegistryReader := newFakeAVSRegistryReader() 77 | 78 | logger := testutils.GetTestLogger() 79 | economicCollector := NewCollector(elReader, avsRegistryReader, "testavs", logger, operatorAddr, quorumNames) 80 | 81 | count := testutil.CollectAndCount(economicCollector, "eigen_slashing_status", "eigen_registered_stakes") 82 | // 1 for eigen_slashing_status, and 2 for eigen_registered_stakes (1 per quorum) 83 | assert.Equal(t, 2, count) 84 | // Comment by shrimalmadhur: A lot has changed recently and I am not sure yet how to fix this test but I am just 85 | // fixing to make sure 86 | // core contract bindings works and we fix unblock slashing release. 87 | // We will come back at this and fix it. 88 | //assert.Equal(t, 3, count) 89 | } 90 | -------------------------------------------------------------------------------- /metrics/collectors/rpc_calls/rpc_calls.go: -------------------------------------------------------------------------------- 1 | package rpccalls 2 | 3 | import ( 4 | "github.com/Layr-Labs/eigensdk-go/types" 5 | "github.com/prometheus/client_golang/prometheus" 6 | "github.com/prometheus/client_golang/prometheus/promauto" 7 | ) 8 | 9 | type CollectorInterface interface { 10 | ObserveRPCRequestDurationSeconds(duration float64, method, clientVersion string) 11 | AddRPCRequestTotal(method, clientVersion string) 12 | } 13 | 14 | // Collector contains instrumented metrics that should be incremented by the avs node using the methods below 15 | // it is used by the eigensdk's instrumented_client, but can also be used by avs teams to instrument their own client 16 | // if it differs from ours. 17 | type Collector struct { 18 | rpcRequestDurationSeconds *prometheus.HistogramVec 19 | rpcRequestTotal *prometheus.CounterVec 20 | } 21 | 22 | // NewCollector returns an rpccalls Collector that collects metrics for json-rpc calls 23 | func NewCollector(avsName string, reg prometheus.Registerer) *Collector { 24 | return &Collector{ 25 | rpcRequestDurationSeconds: promauto.With(reg).NewHistogramVec( 26 | prometheus.HistogramOpts{ 27 | Namespace: types.EigenPromNamespace, 28 | Name: "rpc_request_duration_seconds", 29 | Help: "Duration of json-rpc in seconds", 30 | ConstLabels: prometheus.Labels{"avs_name": avsName}, 31 | }, 32 | // client_version is the client name and its current version. We don't separate them because 33 | // this string is returned as a single string by the web3_clientVersion jsonrpc call 34 | // which doesn't follow any standardized format so not possible to parse it... 35 | // https://ethereum.org/en/developers/docs/apis/json-rpc/#web3_clientversion 36 | []string{"method", "client_version"}, 37 | ), 38 | rpcRequestTotal: promauto.With(reg).NewCounterVec( 39 | prometheus.CounterOpts{ 40 | Namespace: types.EigenPromNamespace, 41 | Name: "rpc_request_total", 42 | Help: "Total number of json-rpc requests", 43 | ConstLabels: prometheus.Labels{"avs_name": avsName}, 44 | }, 45 | []string{"method", "client_version"}, 46 | ), 47 | } 48 | } 49 | 50 | // ObserveRPCRequestDurationSeconds observes the duration of a json-rpc request 51 | func (c *Collector) ObserveRPCRequestDurationSeconds(duration float64, method, clientVersion string) { 52 | c.rpcRequestDurationSeconds.With(prometheus.Labels{ 53 | "method": method, 54 | "client_version": clientVersion, 55 | }).Observe(duration) 56 | } 57 | 58 | // AddRPCRequestTotal adds a json-rpc request to the total number of requests 59 | func (c *Collector) AddRPCRequestTotal(method, clientVersion string) { 60 | c.rpcRequestTotal.With(prometheus.Labels{ 61 | "method": method, 62 | "client_version": clientVersion, 63 | }).Inc() 64 | } 65 | -------------------------------------------------------------------------------- /metrics/collectors/rpc_calls/rpc_calls_test.go: -------------------------------------------------------------------------------- 1 | package rpccalls 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/prometheus/client_golang/prometheus/testutil" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestRpcCallsCollector(t *testing.T) { 12 | reg := prometheus.NewRegistry() 13 | rpcCallsCollector := NewCollector("testavs", reg) 14 | 15 | rpcCallsCollector.ObserveRPCRequestDurationSeconds(1, "testmethod", "testclient/testversion") 16 | // TODO(samlaf): not sure how to test histogram values.. there's no mention of histograms in 17 | // https://pkg.go.dev/github.com/prometheus/client_golang/prometheus/testutil 18 | // assert.Equal(t, 1.0, testutil.ToFloat64(suite.metrics.rpcRequestDurationSeconds)) 19 | 20 | rpcCallsCollector.AddRPCRequestTotal("testmethod", "testclient/testversion") 21 | assert.Equal( 22 | t, 23 | 1.0, 24 | testutil.ToFloat64(rpcCallsCollector.rpcRequestTotal.WithLabelValues("testmethod", "testclient/testversion")), 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /metrics/eigenmetrics_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/Layr-Labs/eigensdk-go/testutils" 11 | 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/prometheus/client_golang/prometheus/testutil" 14 | 15 | "github.com/stretchr/testify/assert" 16 | "github.com/stretchr/testify/suite" 17 | ) 18 | 19 | type MetricsTestSuite struct { 20 | suite.Suite 21 | reg *prometheus.Registry 22 | metrics *EigenMetrics 23 | } 24 | 25 | // this runs before all tests to initialize reg and metrics 26 | func (suite *MetricsTestSuite) SetupTest() { 27 | logger := testutils.GetTestLogger() 28 | // create and start the metrics server 29 | suite.reg = prometheus.NewRegistry() 30 | suite.metrics = NewEigenMetrics("testavs", "localhost:9090", suite.reg, logger) 31 | } 32 | 33 | // here we just check that the metrics server is running and that the metrics are being emitted 34 | func (suite *MetricsTestSuite) TestEigenMetricsServerIntegration() { 35 | errC := suite.metrics.Start(context.Background(), suite.reg) 36 | 37 | // we wait a little bit to make sure the server is running without errors 38 | time.Sleep(1 * time.Second) 39 | if (len(errC)) != 0 { 40 | err := <-errC 41 | if err != nil { 42 | suite.T().Fatal(err) 43 | } 44 | } 45 | 46 | suite.metrics.AddFeeEarnedTotal(1, "testtoken") 47 | suite.metrics.SetPerformanceScore(1) 48 | 49 | resp, err := http.Get("http://localhost:9090/metrics") 50 | assert.NoError(suite.T(), err) 51 | defer resp.Body.Close() 52 | body, err := io.ReadAll(resp.Body) 53 | assert.NoError(suite.T(), err) 54 | 55 | // We only check for "eigen_performance_score" since it's the only metric that doesn't have a label 56 | // the other metrics have labels (they are vectors) so they don't appear in the output unless we use them once at 57 | // least 58 | assert.Contains(suite.T(), string(body), "eigen_fees_earned_total") 59 | assert.Contains(suite.T(), string(body), "eigen_performance_score") 60 | } 61 | 62 | // The below tests are very pedantic but at least show how avs teams can make use of 63 | // the prometheus testutil package to test their metrics code in more involved scenarios 64 | // See https://pkg.go.dev/github.com/prometheus/client_golang@v1.16.0/prometheus/testutil 65 | 66 | func (suite *MetricsTestSuite) TestAddEigenFeeEarnedTotal() { 67 | suite.metrics.AddFeeEarnedTotal(1, "testtoken") 68 | assert.Equal(suite.T(), 1.0, testutil.ToFloat64(suite.metrics.feeEarnedTotal.WithLabelValues("testtoken"))) 69 | } 70 | 71 | func (suite *MetricsTestSuite) TestSetEigenPerformanceScore() { 72 | suite.metrics.SetPerformanceScore(1) 73 | assert.Equal(suite.T(), 1.0, testutil.ToFloat64(suite.metrics.performanceScore)) 74 | } 75 | 76 | // In order for 'go test' to run this suite, we need to create 77 | // a normal test function and pass our suite to suite.Run 78 | func TestMetricsTestSuite(t *testing.T) { 79 | suite.Run(t, new(MetricsTestSuite)) 80 | } 81 | -------------------------------------------------------------------------------- /metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | // Metrics is the interface for the EigenMetrics server 10 | // it only wraps 2 of the 6 methods required by the spec 11 | // (https://docs.eigenlayer.xyz/eigenlayer/avs-guides/spec/metrics/metrics-prom-spec) 12 | // the others are implemented by the economics and rpc_calls collectors, 13 | // and need to be registered with the metrics server prometheus registry 14 | type Metrics interface { 15 | AddFeeEarnedTotal(amount float64, token string) 16 | SetPerformanceScore(score float64) 17 | Start(ctx context.Context, reg prometheus.Gatherer) <-chan error 18 | } 19 | -------------------------------------------------------------------------------- /metrics/noop_metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | type NoopMetrics struct{} 10 | 11 | // enforce that NoopMetrics implements Metrics interface 12 | var _ Metrics = (*NoopMetrics)(nil) 13 | 14 | func NewNoopMetrics() *NoopMetrics { 15 | return &NoopMetrics{} 16 | } 17 | 18 | func (m *NoopMetrics) AddFeeEarnedTotal(amount float64, token string) {} 19 | func (m *NoopMetrics) SetPerformanceScore(score float64) {} 20 | func (m *NoopMetrics) Start(ctx context.Context, reg prometheus.Gatherer) <-chan error { 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /nodeapi/README.md: -------------------------------------------------------------------------------- 1 | # Node Api 2 | 3 | This package implements the eigenlayer avs node spec [node-api](https://docs.eigenlayer.xyz/eigenlayer/avs-guides/spec/api/). 4 | 5 | For an example of how to use this package, see [node-api-example](./nodeapi_example_test.go#L8) -------------------------------------------------------------------------------- /nodeapi/nodeapi_example_test.go: -------------------------------------------------------------------------------- 1 | package nodeapi_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/Layr-Labs/eigensdk-go/logging" 8 | "github.com/Layr-Labs/eigensdk-go/nodeapi" 9 | ) 10 | 11 | func ExampleNodeApi() { 12 | logger, err := logging.NewZapLogger("development") 13 | if err != nil { 14 | fmt.Fprintln(os.Stderr, err) 15 | os.Exit(1) 16 | } 17 | 18 | nodeApi := nodeapi.NewNodeApi("testAvs", "v0.0.1", "localhost:8080", logger) 19 | // register a service with the nodeApi. This could be a db, a cache, a queue, etc. 20 | // see https://docs.eigenlayer.xyz/eigenlayer/avs-guides/spec/api/#get-eigennodeservices 21 | nodeApi.RegisterNewService( 22 | "testServiceId", 23 | "testServiceName", 24 | "testServiceDescription", 25 | nodeapi.ServiceStatusInitializing, 26 | ) 27 | 28 | // this starts the nodeApi server in a goroutine, so no need to wrap it in a go func 29 | nodeApi.Start() 30 | 31 | // ... do other stuff 32 | 33 | // Whenever needed, update the health of the nodeApi or of its backing services 34 | nodeApi.UpdateHealth(nodeapi.PartiallyHealthy) 35 | _ = nodeApi.UpdateServiceStatus("testServiceId", nodeapi.ServiceStatusDown) 36 | _ = nodeApi.DeregisterService("testServiceId") 37 | 38 | } 39 | -------------------------------------------------------------------------------- /services/README.md: -------------------------------------------------------------------------------- 1 | # Services 2 | 3 | eigensdk services are high level APIs that provide backend like functionality for avs nodes and aggregators processes. We provide the following suite of services as soon as possible: 4 | 5 | - [operator pubkeys service](./operatorpubkeys/) 6 | - this service simply indexes the [NewPubkeyRegistration](https://github.com/Layr-Labs/eigenlayer-middleware/blob/9aa6eb543fe38db6e41516f89f15b654ad4d6bf4/src/interfaces/IBLSApkRegistry.sol#L38) events and provides a single endpoint to query for the registered G1 and G2 pubkeys of a given operator address. 7 | - this service is needed for aggregators to get the pubkey of their registered operators so as to verify their signatures 8 | - Registry service 9 | - this service will index the events on the [avs registry contracts](https://github.com/Layr-Labs/eigenlayer-middleware) and provide endpoints to query for the registered avs nodes, their stake at past blocks, operator id, etc. 10 | - this service is needed to be able to check that an eigenlayer operator was registered at a specific block and that the stake threshold had been met for an aggregate bls signature 11 | - Signature aggregation service 12 | - this service will provide endpoints to aggregate operator signatures for various avs tasks 13 | - this service will aggregate signatures in the background and return an aggregated bls signature once it reached a threshold (this will require using the registry service to get operator stakes) 14 | -------------------------------------------------------------------------------- /services/avsregistry/avsregistry.go: -------------------------------------------------------------------------------- 1 | package avsregistry 2 | 3 | import ( 4 | "context" 5 | 6 | opstateretriever "github.com/Layr-Labs/eigensdk-go/contracts/bindings/OperatorStateRetriever" 7 | "github.com/Layr-Labs/eigensdk-go/types" 8 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 9 | ) 10 | 11 | // AvsRegistryServicemService is a service that indexes the Avs Registry contracts and provides a way to query for 12 | // operator state 13 | // at certain blocks, including operatorIds, pubkeys, and staking status in each quorum. 14 | type AvsRegistryService interface { 15 | // GetOperatorsAvsState returns the state of an avs wrt to a list of quorums at a certain block. 16 | // The state includes the operatorId, pubkey, and staking amount in each quorum. 17 | GetOperatorsAvsStateAtBlock( 18 | ctx context.Context, 19 | quorumNumbers types.QuorumNums, 20 | blockNumber types.BlockNum, 21 | ) (map[types.OperatorId]types.OperatorAvsState, error) 22 | // GetQuorumsAvsStateAtBlock returns the aggregated data for a list of quorums at a certain block. 23 | // The aggregated data includes the aggregated pubkey and total stake in each quorum. 24 | // This information is derivable from the Operators Avs State (returned from GetOperatorsAvsStateAtBlock), but this 25 | // function is provided for convenience. 26 | GetQuorumsAvsStateAtBlock( 27 | ctx context.Context, 28 | quorumNumbers types.QuorumNums, 29 | blockNumber types.BlockNum, 30 | ) (map[types.QuorumNum]types.QuorumAvsState, error) 31 | // GetCheckSignaturesIndices returns the registry indices of the nonsigner operators specified by 32 | // nonSignerOperatorIds who were registered at referenceBlockNumber. 33 | GetCheckSignaturesIndices( 34 | opts *bind.CallOpts, 35 | referenceBlockNumber types.BlockNum, 36 | quorumNumbers types.QuorumNums, 37 | nonSignerOperatorIds []types.OperatorId, 38 | ) (opstateretriever.OperatorStateRetrieverCheckSignaturesIndices, error) 39 | } 40 | -------------------------------------------------------------------------------- /services/operatorsinfo/operatorsinfo.go: -------------------------------------------------------------------------------- 1 | package operatorsinfo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Layr-Labs/eigensdk-go/types" 7 | "github.com/ethereum/go-ethereum/common" 8 | ) 9 | 10 | // OperatorsInfoService is a service that indexes the BLSApkRegistry contract and provides a way to query for operator 11 | // pubkeys. 12 | // Currently BLSApkRegistry only stores the hash of the G1 and G2 pubkeys, so this service needs to listen to the 13 | // event NewPubkeyRegistration(address indexed operator, BN254.G1Point pubkeyG1, BN254.G2Point pubkeyG2) 14 | // and store the actual pubkeys, so that AVS aggregators can get the pubkeys of the operators registered with their AVS. 15 | // 16 | // TODO: having this service as a separate service (instead of merged with avsregistry service) is a vestige of the past 17 | // when we had a separate blsPublicKeyCompendium shared contract between all AVSs. We should eventually merge this back 18 | // into avsregistry. 19 | type OperatorsInfoService interface { 20 | // GetOperatorInfo returns the info of the operator with the given address. 21 | // it returns operatorFound=false if the operator is not found. 22 | GetOperatorInfo(ctx context.Context, operator common.Address) (operatorInfo types.OperatorInfo, operatorFound bool) 23 | } 24 | -------------------------------------------------------------------------------- /services/operatorsinfo/operatorsinfo_subgraph_test.go: -------------------------------------------------------------------------------- 1 | package operatorsinfo 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/Layr-Labs/eigensdk-go/logging" 8 | "github.com/Layr-Labs/eigensdk-go/types" 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/shurcooL/graphql" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | type mockGraphQLQuerier struct { 15 | QueryFn func(ctx context.Context, q any, variables map[string]any) error 16 | } 17 | 18 | func (m mockGraphQLQuerier) Query(ctx context.Context, q any, variables map[string]any) error { 19 | return m.QueryFn(ctx, q, variables) 20 | } 21 | 22 | func TestIndexedChainState_GetIndexedOperatorState(t *testing.T) { 23 | logger, err := logging.NewZapLogger(logging.Development) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | operatorAddress := common.Address{0x1} 29 | 30 | operatorsQueryCalled := false 31 | querier := &mockGraphQLQuerier{} 32 | querier.QueryFn = func(ctx context.Context, q any, variables map[string]any) error { 33 | switch res := q.(type) { 34 | case *QueryOperatorByAddressGql: 35 | if operatorsQueryCalled { 36 | return nil 37 | } 38 | res.Operator = IndexedOperatorInfoGql{ 39 | Address: graphql.String(operatorAddress.String()), 40 | PubkeyG1_X: "3336192159512049190945679273141887248666932624338963482128432381981287252980", 41 | PubkeyG1_Y: "15195175002875833468883745675063986308012687914999552116603423331534089122704", 42 | PubkeyG2_X: []graphql.String{ 43 | "21597023645215426396093421944506635812143308313031252511177204078669540440732", 44 | "11405255666568400552575831267661419473985517916677491029848981743882451844775", 45 | }, 46 | PubkeyG2_Y: []graphql.String{ 47 | "9416989242565286095121881312760798075882411191579108217086927390793923664442", 48 | "13612061731370453436662267863740141021994163834412349567410746669651828926551", 49 | }, 50 | SocketUpdates: []SocketUpdates{{Socket: "localhost:32006;32007"}}, 51 | } 52 | operatorsQueryCalled = true 53 | return nil 54 | default: 55 | return nil 56 | } 57 | } 58 | 59 | cs := NewOperatorsInfoServiceSubgraph(context.Background(), querier, logger) 60 | operatorPubkeys, success := cs.GetOperatorInfo(context.Background(), operatorAddress) 61 | assert.True(t, success) 62 | assert.Equal(t, operatorPubkeys.Socket, types.Socket("localhost:32006;32007")) 63 | } 64 | -------------------------------------------------------------------------------- /signer/bls/cerberus/cerberus_test.go: -------------------------------------------------------------------------------- 1 | package cerberus 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | // TODO: add test for cerberus when the docker is released 10 | 11 | func TestOperatorId(t *testing.T) { 12 | signer := Signer{ 13 | pubKeyHex: "dc8f9427033e5ff392f5cc97cc3d6a5472cff2778eee0961a497bd7dbb629a36", 14 | } 15 | 16 | operatorId, err := signer.GetOperatorId() 17 | assert.NoError(t, err) 18 | assert.Equal(t, operatorId, "0x2f6d54c4ef253d4ac1b6bfa672a8f449a0aa6ab1c20ee97a74f8e521fe57604b") 19 | } 20 | -------------------------------------------------------------------------------- /signer/bls/local/local.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | 7 | sdkBls "github.com/Layr-Labs/eigensdk-go/crypto/bls" 8 | "github.com/Layr-Labs/eigensdk-go/signer/bls/types" 9 | ) 10 | 11 | type Config struct { 12 | Path string 13 | Password string 14 | } 15 | 16 | type Signer struct { 17 | key *sdkBls.KeyPair 18 | } 19 | 20 | func New(cfg Config) (*Signer, error) { 21 | keyPair, err := sdkBls.ReadPrivateKeyFromFile(cfg.Path, cfg.Password) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return &Signer{ 26 | key: keyPair, 27 | }, nil 28 | } 29 | 30 | func (s Signer) Sign(ctx context.Context, msg []byte) ([]byte, error) { 31 | if len(msg) != 32 { 32 | return nil, types.ErrInvalidMessageLength 33 | } 34 | 35 | var data [32]byte 36 | copy(data[:], msg) 37 | 38 | return s.key.SignMessage(data).Serialize(), nil 39 | } 40 | 41 | func (s Signer) SignG1(ctx context.Context, msg []byte) ([]byte, error) { 42 | if len(msg) != 64 { 43 | return nil, types.ErrInvalidMessageLength 44 | } 45 | 46 | msgG1 := new(sdkBls.G1Point) 47 | msgG1 = msgG1.Deserialize(msg) 48 | return s.key.SignHashedToCurveMessage(msgG1.G1Affine).Serialize(), nil 49 | } 50 | 51 | func (s Signer) GetOperatorId() (string, error) { 52 | return s.key.PubKey.GetOperatorID(), nil 53 | } 54 | 55 | func (s Signer) GetPublicKeyG1() string { 56 | rawBytes := s.key.PubKey.Bytes() 57 | return hex.EncodeToString(rawBytes[:]) 58 | } 59 | 60 | func (s Signer) GetPublicKeyG2() string { 61 | rawBytes := s.key.GetPubKeyG2().Bytes() 62 | return hex.EncodeToString(rawBytes[:]) 63 | } 64 | -------------------------------------------------------------------------------- /signer/bls/local/local_test.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | // empty password for local testing 12 | const password = "" 13 | 14 | func TestOperatorId(t *testing.T) { 15 | signer, err := New(Config{ 16 | Path: "testdata/test.bls.json", 17 | Password: password, 18 | }) 19 | assert.NoError(t, err) 20 | 21 | operatorId, err := signer.GetOperatorId() 22 | assert.NoError(t, err) 23 | assert.Equal(t, operatorId, "0x2f6d54c4ef253d4ac1b6bfa672a8f449a0aa6ab1c20ee97a74f8e521fe57604b") 24 | } 25 | 26 | func TestPublicKeyG1(t *testing.T) { 27 | signer, err := New(Config{ 28 | Path: "testdata/test.bls.json", 29 | Password: password, 30 | }) 31 | assert.NoError(t, err) 32 | 33 | pubKeyG1 := signer.GetPublicKeyG1() 34 | assert.Equal(t, pubKeyG1, "dc8f9427033e5ff392f5cc97cc3d6a5472cff2778eee0961a497bd7dbb629a36") 35 | } 36 | 37 | func TestPublicKeyG2(t *testing.T) { 38 | signer, err := New(Config{ 39 | Path: "testdata/test.bls.json", 40 | Password: password, 41 | }) 42 | assert.NoError(t, err) 43 | 44 | pubKeyG2 := signer.GetPublicKeyG2() 45 | assert.Equal( 46 | t, 47 | pubKeyG2, 48 | "e9b0f889a847f8dc2ed0514a6b7e11043679491052502e0a68ccc9a410f524e01e9d4863b49ca41d0a94928290b15aed25bfe097e266bdbb9106a09f689b4ea8", 49 | ) 50 | } 51 | 52 | func TestSign(t *testing.T) { 53 | signer, err := New(Config{ 54 | Path: "testdata/test.bls.json", 55 | Password: password, 56 | }) 57 | assert.NoError(t, err) 58 | 59 | msg := [32]byte{} 60 | copy(msg[:], []byte("hello")) 61 | 62 | signature, err := signer.Sign(context.Background(), msg[:]) 63 | signatureHex := hex.EncodeToString(signature) 64 | assert.NoError(t, err) 65 | assert.Equal( 66 | t, 67 | signatureHex, 68 | "2a21c157c51d13aa5922f67751520287088aa5336ae6d2ba06d0423fe7166ed811b48b5a3ff129d4c645f5f4cb33177a3503e26dc2d508b4b943bb8dd45085e2", 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /signer/bls/local/testdata/test.bls.json: -------------------------------------------------------------------------------- 1 | {"pubKey":"E([12918441400833609239596509389345925094276410981682612875742259377217210587702,11932086071418014723502613555349302846398037390325467678628891156883964756680])","crypto":{"cipher":"aes-128-ctr","ciphertext":"42b29d79554441b3c08936a1796929196e59ba9328feaa10441c91be51afd987","cipherparams":{"iv":"13857599ccf21566a5fb960290c20722"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"c0d4f3214d3774751072ca6952a38e4096d6f1131bd60a945e75a2a1e80b5715"},"mac":"49af4e5cdee5089bfbb396fa30cb8ece4e2e57485d2f61e67ef218f0ff7cda07"}} -------------------------------------------------------------------------------- /signer/bls/privatekey/privatekey.go: -------------------------------------------------------------------------------- 1 | package privatekey 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | 7 | sdkBls "github.com/Layr-Labs/eigensdk-go/crypto/bls" 8 | "github.com/Layr-Labs/eigensdk-go/signer/bls/types" 9 | ) 10 | 11 | type Config struct { 12 | PrivateKey string 13 | } 14 | 15 | type Signer struct { 16 | key *sdkBls.KeyPair 17 | } 18 | 19 | func New(cfg Config) (*Signer, error) { 20 | keyPair, err := sdkBls.NewKeyPairFromString(cfg.PrivateKey) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return &Signer{ 25 | key: keyPair, 26 | }, nil 27 | } 28 | 29 | func (s Signer) Sign(ctx context.Context, msg []byte) ([]byte, error) { 30 | if len(msg) != 32 { 31 | return nil, types.ErrInvalidMessageLength 32 | } 33 | 34 | var data [32]byte 35 | copy(data[:], msg) 36 | 37 | return s.key.SignMessage(data).Serialize(), nil 38 | } 39 | 40 | func (s Signer) SignG1(ctx context.Context, msg []byte) ([]byte, error) { 41 | if len(msg) != 64 { 42 | return nil, types.ErrInvalidMessageLength 43 | } 44 | 45 | msgG1 := new(sdkBls.G1Point) 46 | msgG1 = msgG1.Deserialize(msg) 47 | return s.key.SignHashedToCurveMessage(msgG1.G1Affine).Serialize(), nil 48 | } 49 | 50 | func (s Signer) GetOperatorId() (string, error) { 51 | return s.key.PubKey.GetOperatorID(), nil 52 | } 53 | 54 | func (s Signer) GetPublicKeyG1() string { 55 | rawBytes := s.key.PubKey.Bytes() 56 | return hex.EncodeToString(rawBytes[:]) 57 | } 58 | 59 | func (s Signer) GetPublicKeyG2() string { 60 | rawBytes := s.key.GetPubKeyG2().Bytes() 61 | return hex.EncodeToString(rawBytes[:]) 62 | } 63 | -------------------------------------------------------------------------------- /signer/bls/privatekey/privatekey_test.go: -------------------------------------------------------------------------------- 1 | package privatekey 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | const privateKey = "13718104057011380243349384062412322292853638010146548074368241565852610884213" 12 | 13 | func TestOperatorId(t *testing.T) { 14 | signer, err := New(Config{ 15 | PrivateKey: privateKey, 16 | }) 17 | assert.NoError(t, err) 18 | 19 | operatorId, err := signer.GetOperatorId() 20 | assert.NoError(t, err) 21 | assert.Equal(t, operatorId, "0x2f6d54c4ef253d4ac1b6bfa672a8f449a0aa6ab1c20ee97a74f8e521fe57604b") 22 | } 23 | 24 | func TestPublicKeyG1(t *testing.T) { 25 | signer, err := New(Config{ 26 | PrivateKey: privateKey, 27 | }) 28 | assert.NoError(t, err) 29 | 30 | pubKeyG1 := signer.GetPublicKeyG1() 31 | assert.Equal(t, pubKeyG1, "0xdc8f9427033e5ff392f5cc97cc3d6a5472cff2778eee0961a497bd7dbb629a36") 32 | } 33 | 34 | func TestPublicKeyG2(t *testing.T) { 35 | signer, err := New(Config{ 36 | PrivateKey: privateKey, 37 | }) 38 | assert.NoError(t, err) 39 | 40 | pubKeyG2 := signer.GetPublicKeyG2() 41 | assert.Equal( 42 | t, 43 | pubKeyG2, 44 | "0xe9b0f889a847f8dc2ed0514a6b7e11043679491052502e0a68ccc9a410f524e01e9d4863b49ca41d0a94928290b15aed25bfe097e266bdbb9106a09f689b4ea8", 45 | ) 46 | } 47 | 48 | func TestSign(t *testing.T) { 49 | signer, err := New(Config{ 50 | PrivateKey: privateKey, 51 | }) 52 | assert.NoError(t, err) 53 | 54 | msg := [32]byte{} 55 | copy(msg[:], []byte("hello")) 56 | 57 | signature, err := signer.Sign(context.Background(), msg[:]) 58 | signatureHex := hex.EncodeToString(signature) 59 | assert.NoError(t, err) 60 | assert.Equal( 61 | t, 62 | signatureHex, 63 | "2a21c157c51d13aa5922f67751520287088aa5336ae6d2ba06d0423fe7166ed811b48b5a3ff129d4c645f5f4cb33177a3503e26dc2d508b4b943bb8dd45085e2", 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /signer/bls/signer.go: -------------------------------------------------------------------------------- 1 | package bls 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Layr-Labs/eigensdk-go/signer/bls/cerberus" 7 | "github.com/Layr-Labs/eigensdk-go/signer/bls/local" 8 | "github.com/Layr-Labs/eigensdk-go/signer/bls/privatekey" 9 | "github.com/Layr-Labs/eigensdk-go/signer/bls/types" 10 | ) 11 | 12 | type Signer interface { 13 | // Sign signs the message using the BLS signature scheme 14 | Sign(ctx context.Context, msg []byte) ([]byte, error) 15 | 16 | // SignG1 signs the message using the BLS signature scheme 17 | // This message is assumed to be already mapped to G1 point 18 | SignG1(ctx context.Context, msg []byte) ([]byte, error) 19 | 20 | // GetOperatorId returns the operator ID of the signer. 21 | // This is hash of the G1 public key of the signer 22 | GetOperatorId() (string, error) 23 | 24 | GetPublicKeyG1() string 25 | 26 | GetPublicKeyG2() string 27 | } 28 | 29 | // NewSigner creates a new Signer instance based on the provided configuration. 30 | // It supports different types of signers, such as Local and Cerberus. 31 | // 32 | // Parameters: 33 | // - cfg: A SignerConfig struct that contains the configuration for the signer. 34 | // 35 | // Returns: 36 | // - Signer: An instance of the Signer interface. 37 | // - error: An error if the signer type is invalid or if there is an issue creating the signer. 38 | func NewSigner(cfg types.SignerConfig) (Signer, error) { 39 | switch cfg.SignerType { 40 | case types.Local: 41 | return local.New(local.Config{ 42 | Path: cfg.Path, 43 | Password: cfg.Password, 44 | }) 45 | case types.Cerberus: 46 | return cerberus.New(cerberus.Config{ 47 | URL: cfg.CerberusUrl, 48 | PublicKeyHex: cfg.PublicKeyHex, 49 | Password: cfg.CerberusPassword, 50 | EnableTLS: cfg.EnableTLS, 51 | TLSCertFilePath: cfg.TLSCertFilePath, 52 | SignerAPIKey: cfg.CerberusAPIKey, 53 | }) 54 | case types.PrivateKey: 55 | return privatekey.New(privatekey.Config{ 56 | PrivateKey: cfg.PrivateKey, 57 | }) 58 | default: 59 | return nil, types.ErrInvalidSignerType 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /signer/bls/types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrInvalidMessageLength = errors.New("invalid message length. must be 32 bytes") 7 | ErrInvalidSignerType = errors.New("invalid signer type") 8 | ) 9 | -------------------------------------------------------------------------------- /signer/bls/types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type SignerType string 4 | 5 | const ( 6 | // Local signer type 7 | Local SignerType = "local" 8 | // Cerberus signer type 9 | Cerberus SignerType = "cerberus" 10 | // PrivateKey signer type 11 | PrivateKey SignerType = "privatekey" 12 | ) 13 | 14 | type SignerConfig struct { 15 | // PrivateKey is the private key of the signer 16 | PrivateKey string 17 | 18 | // Type of the signer 19 | SignerType SignerType 20 | 21 | // Params for local signer 22 | // Path to the key file 23 | Path string 24 | // Password to decrypt the key file 25 | Password string 26 | 27 | // Params for cerberus signer 28 | // CerberusUrl of the cerberus signer 29 | CerberusUrl string 30 | // PublicKeyHex is the public key of the cerberus signer 31 | PublicKeyHex string 32 | // CerberusPassword is the password to encrypt the key if cerberus using local keystore 33 | CerberusPassword string 34 | // EnableTLS enables TLS for the cerberus signer 35 | EnableTLS bool 36 | // TLSCertFilePath is the path to the TLS cert file 37 | TLSCertFilePath string 38 | // CerberusAPIKey is the API key for the cerberus signer 39 | CerberusAPIKey string 40 | } 41 | -------------------------------------------------------------------------------- /signer/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Layr-Labs/eigensdk-go/signer 2 | 3 | go 1.21.13 4 | toolchain go1.22.5 5 | 6 | replace github.com/Layr-Labs/eigensdk-go => ../../eigensdk-go 7 | 8 | require ( 9 | github.com/Layr-Labs/cerberus-api v0.0.2-0.20250117193600-e69c5e8b08fd 10 | github.com/Layr-Labs/eigensdk-go v0.1.13 11 | github.com/consensys/gnark-crypto v0.12.1 12 | github.com/stretchr/testify v1.9.0 13 | google.golang.org/grpc v1.64.1 14 | ) 15 | 16 | require ( 17 | github.com/bits-and-blooms/bitset v1.13.0 // indirect 18 | github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect 19 | github.com/consensys/bavard v0.1.13 // indirect 20 | github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect 21 | github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/deckarep/golang-set/v2 v2.6.0 // indirect 24 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect 25 | github.com/ethereum/c-kzg-4844 v1.0.0 // indirect 26 | github.com/ethereum/go-ethereum v1.14.13 // indirect 27 | github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 // indirect 28 | github.com/fsnotify/fsnotify v1.6.0 // indirect 29 | github.com/google/uuid v1.6.0 // indirect 30 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect 31 | github.com/holiman/uint256 v1.3.1 // indirect 32 | github.com/mmcloughlin/addchain v0.4.0 // indirect 33 | github.com/pmezard/go-difflib v1.0.0 // indirect 34 | github.com/supranational/blst v0.3.13 // indirect 35 | golang.org/x/crypto v0.31.0 // indirect 36 | golang.org/x/net v0.33.0 // indirect 37 | golang.org/x/sync v0.10.0 // indirect 38 | golang.org/x/sys v0.28.0 // indirect 39 | golang.org/x/text v0.21.0 // indirect 40 | google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect 41 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect 42 | google.golang.org/protobuf v1.34.2 // indirect 43 | gopkg.in/yaml.v3 v3.0.1 // indirect 44 | rsc.io/tmplfunc v0.0.3 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /signerv2/README.md: -------------------------------------------------------------------------------- 1 | # Signer v2 2 | 3 | Signerv2 is a module for signing messages. It provides a simple and unified way to produce cryptographic signatures. 4 | Signers instantiated from this module is required to create some SDK transaction managers (see [`NewPrivateKeyWallet`](../chainio/clients/wallet/privatekey_wallet.go) and [`NewSimpleTxManager`](../chainio/txmgr/simple.go)/[`NewGeometricTxnManager`](../chainio/txmgr/geometric/geometric.go)). 5 | 6 | ## Features 7 | 8 | - Sign messages using raw private keys 9 | - Sign messages using encrypted keystores 10 | - Sign messages using a remote signer (web3 or KMS) 11 | 12 | ### Comparison to Old Signer 13 | 14 | In comparison to the old signer, Signerv2 offers: 15 | 16 | - New signing mechanisms 17 | - A simplified API for easier extension 18 | 19 | ### Using SignerFromConfig 20 | 21 | SignerV2 introduces `SignerFromConfig` 22 | 23 | The `SignerFromConfig` function allows you to create a signer function based on a configuration. 24 | This configuration specifies whether to use a private key signer, a local keystore signer, or a remote web3 signer. 25 | 26 | ```go 27 | package main 28 | 29 | import ( 30 | "github.com/Layr-Labs/eigensdk-go/signerv2" 31 | ) 32 | 33 | func main() { 34 | config := signerv2.Config{ 35 | // ...initialize your configuration... 36 | } 37 | chainID := // Set your chain ID 38 | signerFn, signerAddr, err := signerv2.SignerFromConfig(config, chainID) 39 | if err != nil { 40 | // Handle error 41 | return 42 | } 43 | // Use signerFn and signerAddr as needed 44 | } 45 | ``` 46 | 47 | Internally, `SignerFromConfig` calls different signer functions depending on the config it receives: `PrivateKeySignerFn`, `KeyStoreSignerFn`, or `Web3SignerFn`. 48 | Those functions are also available to users. 49 | 50 | ### KMSSignerFn 51 | 52 | This module includes support for signing messages using a Key Management Service (KMS) key. Use `KMSSignerFn` to create a signer for KMS-managed keys. 53 | 54 | ## Upgrade from Signer (v1) 55 | 56 | `NewPrivateKeySigner` and `NewPrivateKeyFromKeystoreSigner` functions should be upgraded to use the new `SignerFromConfig` and a `Config` with a `PrivateKey`, or `KeystorePath` and `Password`, respectively. 57 | 58 | The functionality given by the `Signer` interface and `BasicSigner` type was redesigned into the [`wallet`](../chainio/clients/wallet) and [`txmgr`](../chainio/txmgr) modules. 59 | After generating a `SignerFn` as specified in ["UsingSignerFromConfig"](#using-signerfromconfig), you can generate a transaction manager via `NewPrivateKeyWallet` and `NewSimpleTxManager` (or `NewGeometricTxnManager` for geometric gas pricing) 60 | -------------------------------------------------------------------------------- /signerv2/config.go: -------------------------------------------------------------------------------- 1 | package signerv2 2 | 3 | import "crypto/ecdsa" 4 | 5 | type Config struct { 6 | PrivateKey *ecdsa.PrivateKey 7 | KeystorePath string 8 | Password string 9 | Endpoint string 10 | Address string 11 | } 12 | 13 | func (c Config) IsPrivateKeySigner() bool { 14 | return c.PrivateKey != nil 15 | } 16 | 17 | func (c Config) IsLocalKeystoreSigner() bool { 18 | return c.KeystorePath != "" 19 | } 20 | 21 | func (c Config) IsWeb3Signer() bool { 22 | if c.Endpoint == "" || c.Address == "" { 23 | return false 24 | } 25 | return true 26 | } 27 | -------------------------------------------------------------------------------- /signerv2/mockdata/dummy.key.json: -------------------------------------------------------------------------------- 1 | { 2 | "crypto": { 3 | "cipher": "aes-128-ctr", 4 | "cipherparams": { 5 | "iv": "6087dab2f9fdbbfaddc31a909735c1e6" 6 | }, 7 | "ciphertext": "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", 8 | "kdf": "pbkdf2", 9 | "kdfparams": { 10 | "c": 262144, 11 | "dklen": 32, 12 | "prf": "hmac-sha256", 13 | "salt": "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" 14 | }, 15 | "mac": "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2" 16 | }, 17 | "id": "3198bc9c-6672-5ab3-d995-4942343ae5b6", 18 | "version": 3 19 | } 20 | -------------------------------------------------------------------------------- /signerv2/signer.go: -------------------------------------------------------------------------------- 1 | package signerv2 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "errors" 7 | "math/big" 8 | 9 | sdkEcdsa "github.com/Layr-Labs/eigensdk-go/crypto/ecdsa" 10 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 11 | "github.com/ethereum/go-ethereum/common" 12 | "github.com/ethereum/go-ethereum/core/types" 13 | "github.com/ethereum/go-ethereum/crypto" 14 | ) 15 | 16 | type SignerFn func(ctx context.Context, address common.Address) (bind.SignerFn, error) 17 | 18 | func PrivateKeySignerFn(privateKey *ecdsa.PrivateKey, chainID *big.Int) (bind.SignerFn, error) { 19 | from := crypto.PubkeyToAddress(privateKey.PublicKey) 20 | signer := types.LatestSignerForChainID(chainID) 21 | 22 | return func(address common.Address, tx *types.Transaction) (*types.Transaction, error) { 23 | if address != from { 24 | return nil, bind.ErrNotAuthorized 25 | } 26 | signature, err := crypto.Sign(signer.Hash(tx).Bytes(), privateKey) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return tx.WithSignature(signer, signature) 31 | }, nil 32 | } 33 | 34 | func KeyStoreSignerFn(path string, password string, chainID *big.Int) (bind.SignerFn, error) { 35 | privateKey, err := sdkEcdsa.ReadKey(path, password) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return PrivateKeySignerFn(privateKey, chainID) 40 | } 41 | 42 | // Web3SignerFn creates a signer function that uses a remote signer 43 | // It exposes `eth_SignTransaction` endpoint which return rlp 44 | // encoded signed tx 45 | func Web3SignerFn(remoteSignerUrl string) (bind.SignerFn, error) { 46 | client := NewWeb3SignerClient(remoteSignerUrl) 47 | 48 | return func(address common.Address, tx *types.Transaction) (*types.Transaction, error) { 49 | return client.SignTransaction(address, tx) 50 | }, nil 51 | } 52 | 53 | func SignerFromConfig(c Config, chainID *big.Int) (SignerFn, common.Address, error) { 54 | var signer SignerFn 55 | var senderAddress common.Address 56 | var err error 57 | if c.IsPrivateKeySigner() { 58 | senderAddress = crypto.PubkeyToAddress(c.PrivateKey.PublicKey) 59 | signer = func(ctx context.Context, address common.Address) (bind.SignerFn, error) { 60 | return PrivateKeySignerFn(c.PrivateKey, chainID) 61 | } 62 | } else if c.IsLocalKeystoreSigner() { 63 | senderAddress, err = sdkEcdsa.GetAddressFromKeyStoreFile(c.KeystorePath) 64 | if err != nil { 65 | return nil, common.Address{}, err 66 | } 67 | signer = func(ctx context.Context, address common.Address) (bind.SignerFn, error) { 68 | return KeyStoreSignerFn(c.KeystorePath, c.Password, chainID) 69 | } 70 | } else if c.IsWeb3Signer() { 71 | senderAddress = common.HexToAddress(c.Address) 72 | signer = func(ctx context.Context, address common.Address) (bind.SignerFn, error) { 73 | return Web3SignerFn(c.Endpoint) 74 | } 75 | } else { 76 | return nil, common.Address{}, errors.New("no signer found") 77 | } 78 | return signer, senderAddress, nil 79 | } 80 | -------------------------------------------------------------------------------- /signerv2/web3_signer.go: -------------------------------------------------------------------------------- 1 | package signerv2 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/Layr-Labs/eigensdk-go/utils" 11 | "github.com/ethereum/go-ethereum/common" 12 | "github.com/ethereum/go-ethereum/core/types" 13 | "github.com/ethereum/go-ethereum/rlp" 14 | "github.com/google/uuid" 15 | ) 16 | 17 | // JsonRpcRequest is a struct for JSON RPC 2.0 request 18 | // See: https://www.jsonrpc.org/specification 19 | type JsonRpcRequest struct { 20 | JsonRPC string `json:"jsonrpc"` 21 | Method string `json:"method"` 22 | Params interface{} `json:"params"` 23 | ID string `json:"id"` 24 | } 25 | 26 | type Web3SignerClient interface { 27 | SignTransaction(from common.Address, tx *types.Transaction) (*types.Transaction, error) 28 | } 29 | 30 | // Web3Signer is a client for a remote signer 31 | // It currently implements `eth_signTransaction` method of Consensys Web3 Signer 32 | // Reference: https://docs.web3signer.consensys.io/reference/api/json-rpc#eth_signtransaction 33 | type Web3Signer struct { 34 | url string 35 | client http.Client 36 | } 37 | 38 | func NewWeb3SignerClient(url string) Web3SignerClient { 39 | client := http.Client{} 40 | return &Web3Signer{client: client, url: url} 41 | } 42 | 43 | func (r Web3Signer) SignTransaction( 44 | from common.Address, 45 | tx *types.Transaction, 46 | ) (*types.Transaction, error) { 47 | method := "eth_signTransaction" 48 | id := uuid.New().String() 49 | params := []map[string]string{ 50 | { 51 | "from": from.Hex(), 52 | "to": tx.To().Hex(), 53 | "gas": utils.Add0x(fmt.Sprintf("%x", tx.Gas())), 54 | "gasPrice": utils.Add0x(hex.EncodeToString(tx.GasPrice().Bytes())), 55 | "nonce": utils.Add0x(fmt.Sprintf("%x", tx.Nonce())), 56 | "data": utils.Add0x(hex.EncodeToString(tx.Data())), 57 | }, 58 | } 59 | 60 | request := JsonRpcRequest{ 61 | JsonRPC: "2.0", 62 | Method: method, 63 | Params: params, 64 | ID: id, 65 | } 66 | 67 | jsonData, err := json.Marshal(request) 68 | if err != nil { 69 | return nil, utils.WrapError("error marshalling request", err) 70 | } 71 | 72 | resp, err := r.client.Post(r.url, "application/json", bytes.NewBuffer(jsonData)) 73 | if err != nil { 74 | return nil, err 75 | } 76 | defer resp.Body.Close() 77 | 78 | var result map[string]interface{} 79 | if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { 80 | return nil, utils.WrapError("error decoding response", err) 81 | } 82 | 83 | if result["error"] != nil { 84 | return nil, utils.WrapError("error in response", fmt.Errorf("%v", result["error"])) 85 | } 86 | 87 | rlpEncodedSignedTx := result["result"].(string) 88 | rlpEncodedSignedTx = utils.Trim0x(rlpEncodedSignedTx) 89 | signedTxBytes, err := hex.DecodeString(rlpEncodedSignedTx) 90 | if err != nil { 91 | return nil, err 92 | } 93 | var signedTx types.Transaction 94 | err = rlp.DecodeBytes(signedTxBytes, &signedTx) 95 | if err != nil { 96 | return nil, err 97 | } 98 | return &signedTx, nil 99 | } 100 | -------------------------------------------------------------------------------- /testutils/crypto.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/ethereum/go-ethereum/crypto" 8 | ) 9 | 10 | func NewEcdsaSkAndAddress() (*ecdsa.PrivateKey, common.Address, error) { 11 | ecdsaSk, err := crypto.GenerateKey() 12 | if err != nil { 13 | return nil, common.Address{}, err 14 | } 15 | pk := ecdsaSk.Public() 16 | address := crypto.PubkeyToAddress(*pk.(*ecdsa.PublicKey)) 17 | return ecdsaSk, address, nil 18 | } 19 | 20 | func ZeroAddress() *common.Address { 21 | return &common.Address{} 22 | } 23 | -------------------------------------------------------------------------------- /testutils/localstack.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/Layr-Labs/eigensdk-go/aws" 8 | "github.com/Layr-Labs/eigensdk-go/utils" 9 | "github.com/aws/aws-sdk-go-v2/service/kms" 10 | "github.com/aws/aws-sdk-go-v2/service/kms/types" 11 | "github.com/testcontainers/testcontainers-go" 12 | "github.com/testcontainers/testcontainers-go/wait" 13 | ) 14 | 15 | const LocalStackPort = "4566" 16 | 17 | func StartLocalstackContainer(name string) (testcontainers.Container, error) { 18 | fmt.Println("Starting Localstack container") 19 | req := testcontainers.ContainerRequest{ 20 | Image: "localstack/localstack:latest", 21 | Name: fmt.Sprintf("localstack-test-%s", name), 22 | Env: map[string]string{ 23 | "LOCALSTACK_HOST": fmt.Sprintf("localhost.localstack.cloud:%s", LocalStackPort), 24 | }, 25 | ExposedPorts: []string{LocalStackPort}, 26 | WaitingFor: wait.ForLog("Ready."), 27 | AutoRemove: true, 28 | } 29 | return testcontainers.GenericContainer(context.Background(), testcontainers.GenericContainerRequest{ 30 | ContainerRequest: req, 31 | Started: true, 32 | }) 33 | } 34 | 35 | func NewKMSClient(localStackPort string) (*kms.Client, error) { 36 | cfg, err := aws.GetAWSConfig( 37 | "localstack", 38 | "localstack", 39 | "us-east-1", 40 | fmt.Sprintf("http://127.0.0.1:%s", localStackPort), 41 | ) 42 | if err != nil { 43 | return nil, utils.WrapError("failed to load AWS config", err) 44 | } 45 | 46 | c := kms.NewFromConfig(*cfg) 47 | return c, nil 48 | } 49 | 50 | func CreateKMSKey(localStackPort string) (*types.KeyMetadata, error) { 51 | c, err := NewKMSClient(localStackPort) 52 | if err != nil { 53 | return nil, utils.WrapError("failed to create KMS client", err) 54 | } 55 | res, err := c.CreateKey(context.Background(), &kms.CreateKeyInput{ 56 | KeySpec: types.KeySpecEccSecgP256k1, 57 | KeyUsage: types.KeyUsageTypeSignVerify, 58 | }) 59 | if err != nil { 60 | return nil, utils.WrapError("failed to create key", err) 61 | } 62 | return res.KeyMetadata, nil 63 | } 64 | -------------------------------------------------------------------------------- /testutils/logger.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | 7 | "github.com/Layr-Labs/eigensdk-go/logging" 8 | ) 9 | 10 | func GetTestLogger() logging.Logger { 11 | return logging.NewTextSLogger(os.Stdout, 12 | &logging.SLoggerOptions{ 13 | Level: slog.LevelDebug, 14 | AddSource: true, 15 | }, 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /testutils/logging.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | 7 | "github.com/Layr-Labs/eigensdk-go/logging" 8 | ) 9 | 10 | // NewTestLogger is just a utility function to create a logger for testing 11 | // It returns an slog textHandler logger that outputs to os.Stdout, with source code information and debug level 12 | func NewTestLogger() logging.Logger { 13 | // we don't use colors because the test output panel in vscode doesn't support them 14 | return logging.NewTextSLogger( 15 | os.Stdout, 16 | &logging.SLoggerOptions{AddSource: true, Level: slog.LevelDebug, NoColor: true}, 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /testutils/test_data.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | // Test data for generic loading of JSON data files. 11 | type TestData[T any] struct { 12 | Input T `json:"input"` 13 | } 14 | 15 | // Create a new instance of `TestData` with the given input data. 16 | func NewTestData[T any](defaultInput T) TestData[T] { 17 | path, exists := os.LookupEnv("TEST_DATA_PATH") 18 | if exists { 19 | file, err := os.ReadFile(filepath.Clean(path)) 20 | if err != nil { 21 | log.Fatalf("Failed to open file: %v", err) 22 | } 23 | 24 | var testData TestData[T] 25 | err = json.Unmarshal(file, &testData) 26 | if err != nil { 27 | log.Fatalf("Failed to decode JSON: %v", err) 28 | } 29 | return testData 30 | } 31 | return TestData[T]{Input: defaultInput} 32 | } 33 | -------------------------------------------------------------------------------- /types/avs.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/Layr-Labs/eigensdk-go/crypto/bls" 7 | ) 8 | 9 | type TaskIndex = uint32 10 | type TaskResponseDigest = Bytes32 11 | type TaskResponse = interface{} 12 | 13 | type TaskResponseHashFunction func(taskResponse TaskResponse) (TaskResponseDigest, error) 14 | 15 | type SignedTaskResponseDigest struct { 16 | TaskResponse TaskResponse 17 | BlsSignature *bls.Signature 18 | OperatorId OperatorId 19 | SignatureVerificationErrorC chan<- error `json:"-"` // removed from json because channels are not marshallable 20 | } 21 | 22 | func (strd SignedTaskResponseDigest) LogValue() slog.Value { 23 | return slog.GroupValue( 24 | slog.Any("taskResponse", strd.TaskResponse), 25 | slog.Any("blsSignature", strd.BlsSignature), 26 | slog.Any("operatorId", strd.OperatorId), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /types/basic.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // This file defines internal types that have custom LogValues (for go's slog), to make debugging easier 4 | 5 | import ( 6 | "encoding/hex" 7 | "log/slog" 8 | ) 9 | 10 | // make TaskResponseDigests print as hex encoded strings instead of a sequence of bytes 11 | type Bytes32 [32]byte 12 | 13 | func (m Bytes32) LogValue() slog.Value { 14 | return slog.StringValue(hex.EncodeToString(m[:])) 15 | } 16 | 17 | func (m *Bytes32) UnderlyingType() [32]byte { 18 | return *m 19 | } 20 | -------------------------------------------------------------------------------- /types/constants.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | const EigenPromNamespace = "eigen" 4 | -------------------------------------------------------------------------------- /types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | var ( 9 | ErrInvalidOperatorAddress = errors.New("invalid operator address") 10 | ErrInvalidEarningsReceiverAddress = errors.New("invalid EarningsReceiverAddress address") 11 | ErrInvalidDelegationApproverAddress = fmt.Errorf( 12 | "invalid DelegationApproverAddress address, it should be either %s or a valid non zero ethereum address", 13 | ZeroAddress, 14 | ) 15 | 16 | ErrInvalidName = errors.New("name is invalid") 17 | ErrInvalidDescription = errors.New("description is invalid") 18 | ErrLogoRequired = errors.New("logo is required") 19 | ErrInvalidWebsiteUrl = errors.New("invalid website url") 20 | ErrInvalidTwitterUrl = errors.New("invalid twitter url") 21 | 22 | ErrInvalidMetadataUrl = errors.New("invalid metadata url") 23 | ErrUnmarshalOperatorMetadata = errors.New("unable to unmarshal operator metadata") 24 | ErrReadingMetadataUrlResponse = errors.New("error reading metadata url body") 25 | ) 26 | -------------------------------------------------------------------------------- /types/operator_metadata.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/Layr-Labs/eigensdk-go/utils" 5 | ) 6 | 7 | // OperatorMetadata is the metadata operator uploads while registering 8 | // itself to EigenLayer 9 | type OperatorMetadata struct { 10 | 11 | // Name of the operator 12 | // It is a required field 13 | Name string `yaml:"name" json:"name"` 14 | 15 | // Website of the operator 16 | // It is a required field 17 | Website string `yaml:"website" json:"website"` 18 | 19 | // Description of the operator. There is a 200-character limit 20 | // It is a required field 21 | Description string `yaml:"description" json:"description"` 22 | 23 | // Logo of the operator. This should be a link to a image file 24 | // which is publicly accessible 25 | // It is a required field 26 | Logo string `yaml:"logo" json:"logo"` 27 | 28 | // Twitter handle of the operator 29 | // It is an optional field 30 | Twitter string `yaml:"twitter" json:"twitter"` 31 | } 32 | 33 | func (om *OperatorMetadata) Validate() error { 34 | err := utils.ValidateText(om.Name) 35 | if err != nil { 36 | return utils.WrapError(ErrInvalidName, err) 37 | } 38 | 39 | err = utils.ValidateText(om.Description) 40 | if err != nil { 41 | return utils.WrapError(ErrInvalidDescription, err) 42 | } 43 | 44 | if len(om.Logo) == 0 { 45 | return ErrLogoRequired 46 | } 47 | 48 | if err = utils.IsImageURL(om.Logo); err != nil { 49 | return err 50 | } 51 | 52 | if len(om.Website) != 0 { 53 | err = utils.CheckIfUrlIsValid(om.Website) 54 | if err != nil { 55 | return utils.WrapError(ErrInvalidWebsiteUrl, err) 56 | } 57 | } 58 | 59 | if len(om.Twitter) != 0 { 60 | err := utils.CheckIfValidTwitterURL(om.Twitter) 61 | if err != nil { 62 | return utils.WrapError(ErrInvalidTwitterUrl, err) 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /types/test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "github.com/Layr-Labs/eigensdk-go/crypto/bls" 4 | 5 | type TestOperator struct { 6 | OperatorId OperatorId 7 | StakePerQuorum map[QuorumNum]StakeAmount 8 | BlsKeypair *bls.KeyPair 9 | Socket Socket 10 | } 11 | -------------------------------------------------------------------------------- /utils/errors.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | ErrInvalidUrl = errors.New("invalid url") 11 | ErrInvalidGithubRawUrl = errors.New("invalid github raw url") 12 | ErrInvalidText = fmt.Errorf("invalid text format, doesn't conform to regex %s", TextRegex) 13 | ErrTextTooLong = func(limit int) error { 14 | return fmt.Errorf("text should be less than %d characters", limit) 15 | } 16 | ErrEmptyText = errors.New("text is empty") 17 | ErrInvalidImageExtension = errors.New( 18 | "invalid image extension. only " + strings.Join(ImageExtensions, ",") + " is supported", 19 | ) 20 | ErrInvalidImageMimeType = errors.New("invalid image mime-type. only png is supported") 21 | ErrInvalidUrlLength = errors.New("url length should be no larger than 1024 character") 22 | ErrUrlPointingToLocalServer = errors.New("url should not point to local server") 23 | ErrEmptyUrl = errors.New("url is empty") 24 | ErrInvalidTwitterUrlRegex = errors.New( 25 | "invalid twitter url, it should be of the format https://twitter.com/ or https://x.com/", 26 | ) 27 | ErrResponseTooLarge = errors.New("response too large, allowed size is 1 MB") 28 | ) 29 | 30 | func TypedErr(e interface{}) error { 31 | switch t := e.(type) { 32 | case error: 33 | return t 34 | case string: 35 | return errors.New(t) 36 | default: 37 | return nil 38 | } 39 | } 40 | 41 | func WrapError(mainErr interface{}, subErr interface{}) error { 42 | var main, sub error 43 | main = TypedErr(mainErr) 44 | sub = TypedErr(subErr) 45 | // Some times the wrap will wrap a nil error 46 | if main == nil && sub == nil { 47 | return nil 48 | } 49 | 50 | if main == nil && sub != nil { 51 | return sub 52 | } 53 | 54 | if main != nil && sub == nil { 55 | return main 56 | } 57 | 58 | return fmt.Errorf("%w: %w", main, sub) 59 | } 60 | --------------------------------------------------------------------------------