├── .codeflow.yml ├── .eslintrc.json ├── .github ├── pull_request_template.md └── workflows │ ├── lint.yaml │ ├── release.yaml │ ├── salus-scan.yml │ └── test.yaml ├── .gitignore ├── .mocharc.json ├── .nycrc ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs └── images │ └── banner.svg ├── examples ├── cosmos │ ├── list-rewards.ts │ └── list-stakes.ts ├── ethereum │ ├── create-and-process-workflow.ts │ ├── create-workflow.ts │ ├── list-rewards │ │ ├── partial-eth.ts │ │ └── validator.ts │ └── list-stakes.ts ├── example.ts └── solana │ ├── create-and-process-workflow.ts │ ├── create-workflow.ts │ └── list-rewards.ts ├── package-lock.json ├── package.json ├── protos ├── buf.gen.orchestration.yaml └── buf.gen.rewards.yaml ├── scripts └── generate-client.sh ├── src ├── auth │ ├── jwt.test.ts │ └── jwt.ts ├── client │ ├── protocols │ │ ├── ethereum-kiln-staking.test.ts │ │ ├── ethereum-kiln-staking.ts │ │ ├── solana-staking.test.ts │ │ └── solana-staking.ts │ ├── staking-client.test.ts │ └── staking-client.ts ├── gen │ ├── coinbase │ │ └── staking │ │ │ ├── orchestration │ │ │ └── v1 │ │ │ │ ├── action.pb.ts │ │ │ │ ├── api.pb.ts │ │ │ │ ├── common.pb.ts │ │ │ │ ├── ethereum.pb.ts │ │ │ │ ├── ethereum_kiln.pb.ts │ │ │ │ ├── network.pb.ts │ │ │ │ ├── protocol.pb.ts │ │ │ │ ├── solana.pb.ts │ │ │ │ ├── staking_context.pb.ts │ │ │ │ ├── staking_target.pb.ts │ │ │ │ └── workflow.pb.ts │ │ │ └── rewards │ │ │ └── v1 │ │ │ ├── common.pb.ts │ │ │ ├── portfolio.pb.ts │ │ │ ├── protocol.pb.ts │ │ │ ├── reward.pb.ts │ │ │ ├── reward_service.pb.ts │ │ │ └── stake.pb.ts │ ├── fetch.pb.ts │ ├── google │ │ ├── api │ │ │ ├── annotations.pb.ts │ │ │ ├── client.pb.ts │ │ │ ├── field_behavior.pb.ts │ │ │ ├── http.pb.ts │ │ │ ├── launch_stage.pb.ts │ │ │ └── resource.pb.ts │ │ └── protobuf │ │ │ ├── descriptor.pb.ts │ │ │ ├── duration.pb.ts │ │ │ ├── struct.pb.ts │ │ │ └── timestamp.pb.ts │ └── protoc-gen-openapiv2 │ │ └── options │ │ ├── annotations.pb.ts │ │ └── openapiv2.pb.ts ├── index.test.ts ├── index.ts ├── signers │ ├── ethereum-signer.ts │ ├── index.ts │ ├── solana-signer.ts │ ├── txsigner.test.ts │ └── txsigner.ts └── utils │ ├── date.test.ts │ └── date.ts └── tsconfig.json /.codeflow.yml: -------------------------------------------------------------------------------- 1 | secure: 2 | required_reviews: 1 3 | requires_mfa: true 4 | branches: 5 | - main 6 | auto_assign_reviewers: false 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "prettier", 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | // NOTE: This needs to be last, per documentation: 12 | // https://github.com/prettier/eslint-plugin-prettier?tab=readme-ov-file#configuration-legacy-eslintrc 13 | "plugin:prettier/recommended" 14 | ], 15 | "globals": { 16 | "Atomics": "readonly", 17 | "SharedArrayBuffer": "readonly" 18 | }, 19 | "parser": "@typescript-eslint/parser", 20 | "parserOptions": { 21 | "ecmaVersion": 2018, 22 | "sourceType": "module" 23 | }, 24 | "plugins": [ 25 | "@typescript-eslint", 26 | "filename-rules", 27 | "prettier" 28 | ], 29 | "rules": { 30 | "filename-rules/match": [ 31 | 2, 32 | "kebab-case" 33 | ], 34 | "no-unused-vars": "warn", 35 | "no-constant-condition": "warn", 36 | "no-empty": "warn", 37 | "consistent-return": "error", 38 | "@typescript-eslint/no-empty-function": "warn", 39 | "prettier/prettier": [ 40 | "error", 41 | { 42 | "singleQuote": true 43 | }, 44 | { 45 | "trailingComma": "none" 46 | } 47 | ], 48 | "prefer-const": "off", 49 | // NOTE: We can't enable TS compilation to enforce function return types 50 | // Source: https://github.com/Microsoft/TypeScript/issues/18529 51 | // So instead we enforce it with linting 52 | "@typescript-eslint/explicit-function-return-type": "error", 53 | "object-curly-spacing": [ 54 | "error", 55 | "always" 56 | ], 57 | "@typescript-eslint/padding-line-between-statements": [ 58 | "error", 59 | { 60 | "blankLine": "always", 61 | "prev": [ 62 | "const", 63 | "let", 64 | "var" 65 | ], 66 | "next": "*" 67 | }, 68 | { 69 | "blankLine": "any", 70 | "prev": [ 71 | "const", 72 | "let", 73 | "var" 74 | ], 75 | "next": [ 76 | "const", 77 | "let", 78 | "var" 79 | ] 80 | } 81 | ], 82 | "lines-between-class-members": [ 83 | "error", 84 | "always", 85 | { 86 | "exceptAfterSingleLine": true 87 | } 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description Of Change 2 | 3 | 4 | 5 | ## Testing Procedure 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: TypeScript Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | lint: 14 | name: lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: "23.x" 21 | registry-url: "https://registry.npmjs.org" 22 | - name: Run npm ci 23 | run: npm ci 24 | - name: Run linter 25 | run: npm run lint 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Version 🔖 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "package.json" 9 | 10 | concurrency: ${{ github.workflow }}-${{ github.ref }} 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | environment: release 16 | permissions: 17 | contents: write 18 | id-token: write 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v4 23 | 24 | - name: Get version from package.json 25 | id: package_version 26 | run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT 27 | 28 | - name: Check if tag exists 29 | id: check_tag 30 | run: | 31 | git fetch --tags 32 | if git rev-parse "v${{ steps.package_version.outputs.VERSION }}" >/dev/null 2>&1; then 33 | echo "::set-output name=EXISTS::true" 34 | fi 35 | 36 | - name: Create Release 37 | if: steps.check_tag.outputs.EXISTS != 'true' 38 | uses: softprops/action-gh-release@v2 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | with: 42 | tag_name: v${{ steps.package_version.outputs.VERSION }} 43 | name: Release v${{ steps.package_version.outputs.VERSION }} 44 | draft: false 45 | prerelease: false 46 | generate_release_notes: true 47 | make_latest: true 48 | 49 | - name: Setup node 50 | uses: actions/setup-node@v4 51 | with: 52 | node-version: "23.x" 53 | registry-url: "https://registry.npmjs.org" 54 | 55 | - name: Publish to NPM 56 | if: steps.check_tag.outputs.EXISTS != 'true' 57 | run: | 58 | npm install -g npm@^9.5.0 59 | npm ci 60 | npm publish --provenance --access public 61 | env: 62 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 63 | -------------------------------------------------------------------------------- /.github/workflows/salus-scan.yml: -------------------------------------------------------------------------------- 1 | name: Salus Security Scan 2 | 3 | on: [pull_request, push] 4 | permissions: 5 | contents: read 6 | jobs: 7 | scan: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Salus Scan 12 | id: salus_scan 13 | uses: federacy/scan-action@0.1.4 14 | with: 15 | active_scanners: "\n - PatternSearch\n - Semgrep\n - Trufflehog" 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: TypeScript Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | name: test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: "23.x" 18 | registry-url: "https://registry.npmjs.org" 19 | - name: Run npm ci 20 | run: npm ci 21 | - name: Run tests 22 | run: npm run test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE/Tools 2 | .docker 3 | .idea 4 | # vim 5 | .*.sw? 6 | 7 | # Local 8 | .DS_Store 9 | 10 | # Binaries for programs and plugins 11 | *.exe 12 | *.exe~ 13 | *.dll 14 | *.so 15 | *.dylib 16 | 17 | # Test binary, built with `go test -c` 18 | *.test 19 | 20 | # Output of the go coverage tool, specifically when used with LiteIDE 21 | *.out 22 | 23 | # Dependency directories (remove the comment below to include it) 24 | vendor/ 25 | 26 | # Cloud API key 27 | .coinbase_cloud_api_key*.json 28 | 29 | # Example binary 30 | staking-client-* 31 | 32 | # environment variables 33 | .env 34 | .envrc 35 | 36 | # Output directory 37 | dist/ 38 | 39 | node_modules/ 40 | 41 | docs/**/*.json 42 | 43 | # code coverage output 44 | coverage 45 | .nyc_output 46 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "ts-node/register" 4 | ], 5 | "spec": "src/**/*.test.ts" 6 | } 7 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "all": true, 4 | "include": [ 5 | "src/**/*.ts" 6 | ], 7 | "exclude": [ 8 | "src/**/*.test.ts", 9 | "src/**/index.ts", 10 | "node_modules", 11 | "src/gen" 12 | ], 13 | "reporter": [ 14 | "text", 15 | "html" 16 | ], 17 | "check-coverage": true, 18 | "lines": 75, 19 | "statements": 75 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdownlint.config": { 3 | "MD033": false, 4 | }, 5 | "editor.formatOnSave": true, 6 | "editor.formatOnPaste": true, 7 | "editor.formatOnType": false, 8 | "files.autoSave": "onFocusChange", 9 | "cSpell.words": [ 10 | "automerge", 11 | "holesky", 12 | "nonroutine", 13 | "solana", 14 | "txsigner", 15 | "unstake" 16 | ], 17 | "typescript.updateImportsOnFileMove.enabled": "always", 18 | "javascript.updateImportsOnFileMove.enabled": "always", 19 | "vs-code-prettier-eslint.prettierLast": false, 20 | "[javascript]": { 21 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" 22 | }, 23 | "[json]": { 24 | "editor.defaultFormatter": "vscode.json-language-features" 25 | }, 26 | "[jsonc]": { 27 | "editor.defaultFormatter": "vscode.json-language-features" 28 | }, 29 | "[typescript]": { 30 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" 31 | }, 32 | "editor.codeActionsOnSave": { 33 | "source.fixAll.eslint": "explicit" 34 | }, 35 | "files.insertFinalNewline": true, 36 | } 37 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct for Staking Client Library TS 2 | 3 | ## 1. Purpose 4 | 5 | This Code of Conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. The primary goal is to create a safe and inclusive environment for everyone, regardless of age, gender, sexual orientation, disability, ethnicity, or religion. 6 | 7 | ## 2. Expected Behavior 8 | 9 | We expect all community members to: 10 | 11 | - Exercise empathy and kindness towards other community members. 12 | - Avoid using offensive or derogatory language. 13 | - Be respectful of differing viewpoints and experiences. 14 | - Give and gracefully accept constructive feedback. 15 | - Focus on what is best for the community as a whole. 16 | 17 | ## 3. Unacceptable Behavior 18 | 19 | Unacceptable behavior from any community member will not be tolerated and includes: 20 | 21 | - Harassment or discrimination based on age, gender, sexual orientation, disability, ethnicity, or religion. 22 | - Use of derogatory, discriminatory, or explicit language. 23 | - Personal attacks or intimidation. 24 | - Public or private harassment. 25 | - Publishing others' private information, such as a physical or email address, without explicit permission. 26 | - Other conduct which could reasonably be considered inappropriate in a professional setting. 27 | 28 | ## 4. Reporting Issues 29 | 30 | If you experience or witness unacceptable behavior—or have any other concerns—please report it by DMing a moderator in the [CDP Discord](https://discord.gg/cdp). All reports will be handled with discretion. 31 | 32 | ## 5. Consequences for Unacceptable Behavior 33 | 34 | Unacceptable behavior from any community member, including project owners and maintainers, will not be tolerated. Anyone asked to stop unacceptable behavior is expected to comply immediately. 35 | 36 | If a community member engages in unacceptable behavior, the project maintainers may take action they deem appropriate, including a temporary ban or permanent expulsion from the community without warning. 37 | 38 | ## 6. Scope 39 | 40 | This Code of Conduct applies within all project spaces, and it also applies when an individual is representing as part of the project or its community in public spaces. 41 | 42 | ## 7. Attribution 43 | 44 | This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at . 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love your input. Whether it be: 4 | 5 | - Reporting a bug 6 | - Submitting a fix 7 | - Proposing new features 8 | - Discussing the state of the code 9 | 10 | All feedback is welcome! 11 | 12 | ## Code of Conduct 13 | 14 | Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. 15 | 16 | ## Official Repository 17 | 18 | We use Github to host code, track issues, and accept pull requests. This Github repository is the "official" repository of this project. All changes/fixes/suggestions should be submitted here (i.e. github.com/coinbase/staking-client-library-ts) 19 | 20 | ## Reporting Bugs 21 | 22 | If you find a bug, please create an issue in our GitHub repository. 23 | 24 | ## Suggesting Enhancements 25 | 26 | If you have an idea for a new feature, or just general feedback, please create an issue in our GitHub repository. 27 | 28 | ## Pull Request Process 29 | 30 | 1. Fork the repository on GitHub. 31 | 2. Clone your fork to your local machine. 32 | 3. Create a new branch on which to make your change, e.g. `git checkout -b my_new_feature`. 33 | 4. Make your change (more tips [below](#making-changes). 34 | 5. Push your change to your fork. 35 | 6. Submit a pull request. 36 | 37 | ### Making Changes 38 | 39 | This repository uses a code generation process based off of protobuf pushed to [the public BSR](https://buf.build/cdp). To ensure the most recent updates are reflected in your changes, regenerate the code by running: 40 | 41 | ```shell 42 | npm run gen 43 | ``` 44 | 45 | Before submitting the PR, ensure the linting CI will pass by running this command and checking for errors: 46 | 47 | ```shell 48 | npm run lint 49 | ``` 50 | 51 | or use this command fix the linting mistakes proactively: 52 | 53 | ```shell 54 | npm run lint-fix 55 | ``` 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (c) 2018-2024 Coinbase, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Coinbase Staking API 2 | 3 | # [Coinbase Staking API](https://github.com/coinbase/staking-client-library-ts) 4 | 5 | > Programmatic access to Coinbase's best-in-class staking infrastructure and services. :large_blue_circle: 6 | 7 | [![npm version](https://badge.fury.io/js/@coinbase%2Fstaking-client-library-ts.svg)](https://badge.fury.io/js/@coinbase%2Fstaking-client-library-ts) [![Current version](https://img.shields.io/github/tag/coinbase/staking-client-library-ts?color=3498DB&label=version)](https://github.com/coinbase/staking-client-library-ts/releases) [![GitHub contributors](https://img.shields.io/github/contributors/coinbase/staking-client-library-ts?color=3498DB)](https://github.com/coinbase/staking-client-library-ts/graphs/contributors) [![GitHub Stars](https://img.shields.io/github/stars/coinbase/staking-client-library-ts.svg?color=3498DB)](https://github.com/coinbase/staking-client-library-ts/stargazers) [![GitHub](https://img.shields.io/github/license/coinbase/staking-client-library-ts?color=3498DB)](https://github.com/coinbase/staking-client-library-ts/blob/main/LICENSE) 8 | 9 | ## Overview 10 | 11 | `staking-client-library-ts` is the Typescript SDK for the **Coinbase Staking API** :large_blue_circle:. 12 | 13 | The Coinbase Staking API empowers developers to deliver a fully-featured staking experience in their Web2 apps, wallets, or dApps using *one common interface* across protocols. 14 | 15 | A traditional infrastructure-heavy staking integration can take months. Coinbase's Staking API enables onboarding within hours :sparkles:. 16 | 17 | ## Quick Start 18 | 19 | Prerequisite: [Node 20+](https://www.npmjs.com/package/node/v/20.11.1) 20 | 21 | 1. In a fresh directory, run: 22 | 23 | ```shell 24 | npm install @coinbase/staking-client-library-ts 25 | ``` 26 | 27 | 2. Copy and paste a code sample from below or any other [example](./examples/) into an `example.ts` file. 28 | 29 | 3. Create a new API Key in the [portal](https://portal.cdp.coinbase.com/access/api) and paste the API Key name and private key into the example. 30 | 31 | 4. Run :rocket: 32 | 33 | ```shell 34 | npx ts-node example.ts 35 | ``` 36 | 37 | ### Stake Partial ETH :diamond_shape_with_a_dot_inside: 38 | 39 | This code sample helps stake partial ETH (non-multiples of 32 ETH). View the full source [here](examples/ethereum/create-workflow.ts) 40 | 41 |
42 | 43 | ```typescript 44 | // examples/ethereum/create-workflow.ts 45 | import { StakingClient } from "@coinbase/staking-client-library-ts"; 46 | 47 | // Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api 48 | const apiKeyName: string = 'your-api-key-name'; 49 | const apiPrivateKey: string = 'your-api-private-key'; 50 | 51 | const client = new StakingClient(apiKeyName, apiPrivateKey); 52 | 53 | client.Ethereum.stake('holesky', '0xdb816889F2a7362EF242E5a717dfD5B38Ae849FE', '123') 54 | .then((workflow) => { 55 | console.log(JSON.stringify(workflow, null, 2)); 56 | }) 57 | .catch(() => { 58 | throw new Error('Error running stake action on ethereum'); 59 | }); 60 | ``` 61 | 62 |
63 | 64 |
65 | Output 66 | 67 | ```text 68 | { 69 | "name": "workflows/baecd951-838f-44ec-b7b5-20e1820c09dc", 70 | "action": "protocols/ethereum_kiln/networks/holesky/actions/stake", 71 | "ethereumKilnStakingParameters": { 72 | "stakeParameters": { 73 | "stakerAddress": "0xdb816889F2a7362EF242E5a717dfD5B38Ae849FE", 74 | "integratorContractAddress": "0xA55416de5DE61A0AC1aa8970a280E04388B1dE4b", 75 | "amount": { 76 | "value": "123", 77 | "currency": "ETH" 78 | } 79 | } 80 | }, 81 | "state": "STATE_WAITING_FOR_EXT_BROADCAST", 82 | "currentStepId": 0, 83 | "steps": [ 84 | { 85 | "name": "stake tx", 86 | "txStepOutput": { 87 | "unsignedTx": "02f3824268068502540be4008503c1b8346683061a8094a55416de5de61a0ac1aa8970a280e04388b1de4b7b843a4b66f1c0808080", 88 | "signedTx": "", 89 | "txHash": "", 90 | "state": "STATE_PENDING_EXT_BROADCAST", 91 | "errorMessage": "" 92 | } 93 | } 94 | ], 95 | "createTime": "2024-05-08T15:24:57.480231386Z", 96 | "updateTime": "2024-05-08T15:24:57.480231386Z", 97 | "completeTime": null 98 | } 99 | ``` 100 | 101 |
102 | 103 | ### Stake SOL :diamond_shape_with_a_dot_inside: 104 | 105 | This code sample helps stake SOL from a given user wallet. View the full source [here](examples/solana/create-workflow.ts) 106 | 107 |
108 | 109 | ```typescript 110 | // examples/solana/create-workflow.ts 111 | import { StakingClient } from "@coinbase/staking-client-library-ts"; 112 | 113 | // Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api 114 | const apiKeyName: string = 'your-api-key-name'; 115 | const apiPrivateKey: string = 'your-api-private-key'; 116 | 117 | const client = new StakingClient(apiKeyName, apiPrivateKey); 118 | 119 | client.Solana.stake('devnet', '8rMGARtkJY5QygP1mgvBFLsE9JrvXByARJiyNfcSE5Z', '100000000') 120 | .then((workflow) => { 121 | console.log(JSON.stringify(workflow, null, 2)); 122 | }) 123 | .catch(() => { 124 | throw new Error('Error running stake action on solana'); 125 | }); 126 | ``` 127 | 128 |
129 | 130 |
131 | Output 132 | 133 | ```text 134 | { 135 | "name": "workflows/2cd484db-56fe-4c8b-a53d-8039c8f27547", 136 | "action": "protocols/solana/networks/devnet/actions/stake", 137 | "solanaStakingParameters": { 138 | "stakeParameters": { 139 | "walletAddress": "8rMGARtkJY5QygP1mgvBFLsE9JrvXByARJiyNfcSE5Z", 140 | "validatorAddress": "GkqYQysEGmuL6V2AJoNnWZUz2ZBGWhzQXsJiXm2CLKAN", 141 | "amount": { 142 | "value": "100000000", 143 | "currency": "SOL" 144 | }, 145 | "priorityFee": { 146 | "computeUnitLimit": "0", 147 | "unitPrice": "0" 148 | } 149 | } 150 | }, 151 | "state": "STATE_WAITING_FOR_EXT_BROADCAST", 152 | "currentStepId": 0, 153 | "steps": [ 154 | { 155 | "name": "stake tx", 156 | "txStepOutput": { 157 | "unsignedTx": "66hEYYWnwGWkGpMKF2H2sCzxnmoAfY8LPnYMgWdY6rC7hX2H6DEE2YdPxECFx8FeeNmea8N87L4KuZ6dirYXZi9XNr5uPJdf8W1jdShcSwzSmmqz4SA7dmFjdTM19hNEu7hMMF7C2Vcm8zka9FErt4wyshJNXYXM6cbJ8UUypGAb8g4vQDMoVavSiVFWxMGE5Sv7JL2gXkFEz2UbxvX7t6W2UbhDtt7545km4rQtFcrMTahmaoqaTMysLuoMcJpzps1c7pCigthYYcBN7yxF4zVZHJHbMXqFuap1BAb2MCYeBxk4krzGJNR3Avo6seVVthxMLHqExv8Yzrdvufn61xv6S4DGQdhbbUM2auGi5b45bkJ4EEHKMhEXqXWrYHSQQMbtgJ2EP4zNiSK8avPREuUQS4BS1aRUF3zT8bkEfWDfp5EjxAs6fumYZCkRKsyjRHEZMN6m9fwESmJqdJPeTJUrZkkvhJZCszPdeTNxSzrUnaeQ2oLvmw29MXVzdvx9gzpa1AKP9YcWjjbZGBkBrYnKzS6KkDBvi2uvo633eqJCrMzRDrVsvQPAi9kTQcqMFt567WotqbF9EBhfAKMss9G9eHXeVCGPa7P2kG9Whix2adaatpi6B6yUjfHFKwXNyXrTUM5UnjCBW9PoLyjPve8q6x6HqVb63v97B29HjguuEZhMjrMctXpPB4EVhemczKitdsYaQRFzsV1R3XVHnfha2BwTyw5B9U7uYFqdrfKwwszni5aqvAsSV3YwGEuwMrZSaCYVub5DtDaqKiJee138tGsn16bg6seb5jZeEiguaAmwDrXY9nT4ihvh4Gqtao4BoipSvb3vQJsjG4KAxTQWb3HFqQXUoVrs81sRh64amtg7or4Pwj8F5fMwx6VyqHW8BbfA4CaXrfunWLKo5Qap1gNnaV9WxoN9n9bKsJ9fS2PQgtX", 158 | "signedTx": "", 159 | "txHash": "", 160 | "state": "STATE_PENDING_EXT_BROADCAST", 161 | "errorMessage": "" 162 | } 163 | } 164 | ], 165 | "createTime": "2024-05-08T15:25:58.265307812Z", 166 | "updateTime": "2024-05-08T15:25:58.265307812Z", 167 | "completeTime": null 168 | } 169 | ``` 170 | 171 |
172 | 173 | ### View Partial ETH Rewards :moneybag: 174 | 175 | This code sample helps view rewards for an Ethereum address. View the full source [here](examples/ethereum/list-rewards/partial-eth.ts). 176 | 177 |
178 | 179 | ```typescript 180 | // examples/ethereum/list-rewards/partial-eth.ts 181 | import { StakingClient } from "@coinbase/staking-client-library-ts"; 182 | 183 | // Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api 184 | const apiKeyName: string = 'your-api-key-name'; 185 | const apiPrivateKey: string = 'your-api-private-key'; 186 | 187 | const client = new StakingClient(apiKeyName, apiPrivateKey); 188 | 189 | // Defines which address and rewards we want to see 190 | const address: string = 191 | '0x60c7e246344ae3856cf9abe3a2e258d495fc39e0'; 192 | const filter: string = `address='${address}' AND period_end_time > '2024-05-01T00:00:00Z' AND period_end_time < '2024-05-02T00:00:00Z'`; 193 | 194 | // Loops through rewards array and prints each reward 195 | var list = async function () { 196 | const resp = await client.Ethereum.listRewards(filter) 197 | resp.rewards!.forEach((reward) => { 198 | console.log(JSON.stringify(reward, null, 2)); 199 | }); 200 | } 201 | 202 | list(); 203 | ``` 204 | 205 |
206 | 207 |
208 | Output 209 | 210 | ```json 211 | { 212 | "address": "0x60c7e246344ae3856cf9abe3a2e258d495fc39e0", 213 | "date": "2024-05-01", 214 | "aggregationUnit": "DAY", 215 | "periodStartTime": "2024-05-01T00:00:00Z", 216 | "periodEndTime": "2024-05-01T23:59:59Z", 217 | "totalEarnedNativeUnit": { 218 | "amount": "0.001212525541415161", 219 | "exp": "18", 220 | "ticker": "ETH", 221 | "rawNumeric": "1212525541415161" 222 | }, 223 | "totalEarnedUsd": [ 224 | { 225 | "source": "COINBASE_EXCHANGE", 226 | "conversionTime": "2024-05-02T00:09:00Z", 227 | "amount": { 228 | "amount": "3.61", 229 | "exp": "2", 230 | "ticker": "USD", 231 | "rawNumeric": "361" 232 | }, 233 | "conversionPrice": "2971.419922" 234 | } 235 | ], 236 | "endingBalance": null, 237 | "protocol": "ethereum", 238 | "rewardState": "PENDING_CLAIMABLE" 239 | } 240 | ``` 241 | 242 |
243 | 244 | ## Documentation 245 | 246 | There are numerous examples in the [`examples directory`](./examples) to help get you started. For even more, refer to our [documentation website](https://docs.cdp.coinbase.com/staking/docs/welcome) for detailed definitions, [API specification](https://docs.cdp.coinbase.com/staking/reference), integration guides, and more! 247 | 248 | ## Contributing 249 | 250 | Thanks for considering contributing to the project! Please refer to [our contribution guide](./CONTRIBUTING.md). 251 | 252 | ## Contact Us 253 | 254 | If you have any questions, please reach out to us in the #staking channel on our [Discord](https://discord.com/invite/cdp) server. 255 | -------------------------------------------------------------------------------- /docs/images/banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/cosmos/list-rewards.ts: -------------------------------------------------------------------------------- 1 | import { StakingClient } from '../../src/client/staking-client'; 2 | 3 | // Address can be substituted with any Cosmos validator. 4 | const address: string = 'cosmosvaloper1c4k24jzduc365kywrsvf5ujz4ya6mwympnc4en'; 5 | 6 | const filter: string = `address='${address}' AND period_end_time > '2024-03-25T00:00:00Z' AND period_end_time < '2024-03-27T00:00:00Z'`; 7 | 8 | // Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api 9 | const apiKeyName: string = 'your-api-key-name'; 10 | const apiPrivateKey: string = 'your-api-private-key'; 11 | 12 | const client = new StakingClient(apiKeyName, apiPrivateKey); 13 | 14 | // Loops through rewards array and prints each reward 15 | client.Cosmos.listRewards(filter).then((resp) => { 16 | resp.rewards!.forEach((reward) => { 17 | console.log(JSON.stringify(reward, null, 2)); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /examples/cosmos/list-stakes.ts: -------------------------------------------------------------------------------- 1 | import { StakingClient } from '../../src/client/staking-client'; 2 | 3 | // Address can be substituted with any Cosmos validator. 4 | const address: string = 'cosmosvaloper1c4k24jzduc365kywrsvf5ujz4ya6mwympnc4en'; 5 | 6 | // Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api 7 | const apiKeyName: string = 'your-api-key-name'; 8 | const apiPrivateKey: string = 'your-api-private-key'; 9 | 10 | const client = new StakingClient(apiKeyName, apiPrivateKey); 11 | 12 | async function listStakes(): Promise { 13 | if (address === '') { 14 | throw new Error('Please set the address variable in this file'); 15 | } 16 | 17 | const filter: string = `address='${address}'`; 18 | 19 | try { 20 | // List cosmos staking balances 21 | let resp = await client.Cosmos.listStakes(filter); 22 | 23 | let count = 0; 24 | 25 | // Loop through staked balance array and print each balance 26 | resp.stakes!.forEach((stake) => { 27 | count++; 28 | const marshaledStake = JSON.stringify(stake); 29 | 30 | console.log(`[${count}] Stake details: ${marshaledStake}`); 31 | }); 32 | } catch (error) { 33 | if (error instanceof Error) { 34 | throw new Error(`Error listing staking balances: ${error.message}`); 35 | } 36 | } 37 | } 38 | 39 | listStakes().catch((error) => { 40 | console.error('Error listing cosmos staking balances: ', error.message); 41 | }); 42 | -------------------------------------------------------------------------------- /examples/ethereum/create-and-process-workflow.ts: -------------------------------------------------------------------------------- 1 | import { TxSignerFactory } from '../../src/signers'; 2 | import { 3 | StakingClient, 4 | workflowHasFinished, 5 | workflowWaitingForExternalBroadcast, 6 | isTxStepOutput, 7 | isWaitStepOutput, 8 | } from '../../src/client/staking-client'; 9 | import { Workflow } from '../../src/gen/coinbase/staking/orchestration/v1/workflow.pb'; 10 | import { calculateTimeDifference } from '../../src/utils/date'; 11 | 12 | const walletPrivateKey: string = 'your-wallet-private-key'; // replace with your wallet's private key 13 | const stakerAddress: string = 'your-wallet-address'; // replace with your staker address 14 | const amount: string = '123'; // replace with your amount 15 | const network: string = 'holesky'; // replace with your network 16 | 17 | // Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api 18 | const apiKeyName: string = 'your-api-key-name'; 19 | const apiPrivateKey: string = 'your-api-private-key'; 20 | 21 | const client = new StakingClient(apiKeyName, apiPrivateKey); 22 | 23 | const signer = TxSignerFactory.getSigner('ethereum'); 24 | 25 | async function stakePartialEth(): Promise { 26 | if (walletPrivateKey === '' || stakerAddress === '') { 27 | throw new Error( 28 | 'Please set the walletPrivateKey and stakerAddress variables in this file', 29 | ); 30 | } 31 | 32 | let unsignedTx = ''; 33 | let workflow: Workflow = {} as Workflow; 34 | let currentStepId: number | undefined; 35 | 36 | try { 37 | // Create a new eth kiln stake workflow 38 | workflow = await client.Ethereum.stake(network, stakerAddress, amount); 39 | 40 | currentStepId = workflow.currentStepId; 41 | if (currentStepId == null) { 42 | throw new Error('Unexpected workflow state. currentStepId is null'); 43 | } 44 | 45 | console.log('Workflow created %s ...', workflow.name); 46 | } catch (error) { 47 | if (error instanceof Error) { 48 | throw new Error(`Error creating workflow: ${error.message}`); 49 | } 50 | throw new Error(`Error creating workflow`); 51 | } 52 | 53 | // Loop until the workflow has reached an end state. 54 | // eslint-disable-next-line no-constant-condition 55 | while (true) { 56 | // Every second, get the latest workflow state. 57 | // If the workflow is waiting for signing, sign the unsigned tx and return back the signed tx. 58 | // If the workflow is waiting for external broadcast, sign and broadcast the unsigned tx externally and return back the tx hash via the PerformWorkflowStep API. 59 | // Note: In this example, we just log this message as the wallet provider needs to implement this logic. 60 | try { 61 | workflow = await client.getWorkflow(workflow.name!); 62 | } catch (error) { 63 | // TODO: add retry logic for network errors 64 | if (error instanceof Error) { 65 | throw new Error(`Error creating workflow: ${error.message}`); 66 | } 67 | } 68 | 69 | await printWorkflowProgressDetails(workflow); 70 | 71 | if (workflowWaitingForExternalBroadcast(workflow)) { 72 | unsignedTx = 73 | workflow.steps![currentStepId].txStepOutput?.unsignedTx || ''; 74 | if (unsignedTx === '') { 75 | console.log('Waiting for unsigned tx to be available ...'); 76 | await new Promise((resolve) => setTimeout(resolve, 1000)); // sleep for 1 second 77 | continue; 78 | } 79 | 80 | console.log('Signing unsigned tx %s ...', unsignedTx); 81 | const signedTx = await signer.signTransaction( 82 | walletPrivateKey, 83 | unsignedTx, 84 | ); 85 | 86 | console.log( 87 | 'Please broadcast this signed tx %s externally and return back the tx hash via the PerformWorkflowStep API ...', 88 | signedTx, 89 | ); 90 | break; 91 | } else if (workflowHasFinished(workflow)) { 92 | console.log('Workflow completed with state %s ...', workflow.state); 93 | break; 94 | } 95 | 96 | await new Promise((resolve) => setTimeout(resolve, 1000)); // sleep for 1 second 97 | } 98 | } 99 | 100 | async function printWorkflowProgressDetails(workflow: Workflow): Promise { 101 | if (workflow.steps == null || workflow.steps.length === 0) { 102 | console.log('Waiting for steps to be created ...'); 103 | await new Promise((resolve) => setTimeout(resolve, 1000)); // sleep for 1 second 104 | return; 105 | } 106 | 107 | const currentStepId = workflow.currentStepId; 108 | 109 | if (currentStepId == null) { 110 | return; 111 | } 112 | 113 | const step = workflow.steps[currentStepId]; 114 | 115 | let stepDetails = ''; 116 | 117 | if (isTxStepOutput(step)) { 118 | stepDetails = `state: ${step.txStepOutput?.state} tx hash: ${step.txStepOutput?.txHash}`; 119 | } else if (isWaitStepOutput(step)) { 120 | stepDetails = `state: ${step.waitStepOutput?.state}} current: ${step.waitStepOutput?.current}} target: ${step.waitStepOutput?.target}`; 121 | } else { 122 | throw new Error('Encountered unexpected workflow step type'); 123 | } 124 | 125 | const runtime = calculateTimeDifference( 126 | workflow.createTime, 127 | workflow.updateTime, 128 | ); 129 | 130 | if (workflowHasFinished(workflow)) { 131 | console.log( 132 | 'Workflow reached end state - step name: %s %s workflow state: %s runtime: %d seconds', 133 | step.name, 134 | stepDetails, 135 | workflow.state, 136 | runtime, 137 | ); 138 | } else { 139 | console.log( 140 | 'Waiting for workflow to finish - step name: %s %s workflow state: %s runtime: %d seconds', 141 | step.name, 142 | stepDetails, 143 | workflow.state, 144 | runtime, 145 | ); 146 | } 147 | } 148 | 149 | stakePartialEth() 150 | .then(() => { 151 | console.log('Done staking eth'); 152 | }) 153 | .catch((error) => { 154 | console.error('Error staking eth: ', error.message); 155 | }); 156 | -------------------------------------------------------------------------------- /examples/ethereum/create-workflow.ts: -------------------------------------------------------------------------------- 1 | import { StakingClient } from '../../src/client/staking-client'; 2 | import { Workflow } from '../../src/gen/coinbase/staking/orchestration/v1/workflow.pb'; 3 | 4 | const stakerAddress: string = '0xdb816889F2a7362EF242E5a717dfD5B38Ae849FE'; // replace with your staker address 5 | const amount: string = '123'; // replace with your amount 6 | const network: string = 'holesky'; // replace with your network 7 | 8 | // Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api 9 | const apiKeyName: string = 'your-api-key-name'; 10 | const apiPrivateKey: string = 'your-api-private-key'; 11 | 12 | const client = new StakingClient(apiKeyName, apiPrivateKey); 13 | 14 | async function stakePartialEth(): Promise { 15 | if (stakerAddress === '') { 16 | throw new Error('Please set the stakerAddress variable in this file'); 17 | } 18 | 19 | let workflow: Workflow = {} as Workflow; 20 | 21 | try { 22 | // Create a new eth kiln stake workflow 23 | workflow = await client.Ethereum.stake(network, stakerAddress, amount); 24 | 25 | console.log(JSON.stringify(workflow, null, 2)); 26 | } catch (error) { 27 | let errorMessage = ''; 28 | 29 | if (error instanceof Error) { 30 | errorMessage = error.message; 31 | } 32 | throw new Error(`Error creating workflow: ${errorMessage}`); 33 | } 34 | } 35 | 36 | stakePartialEth() 37 | .then(() => { 38 | console.log('Done creating eth staking workflow'); 39 | }) 40 | .catch((error) => { 41 | if (error instanceof Error) { 42 | console.error('Error creating eth staking workflow: ', error.message); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /examples/ethereum/list-rewards/partial-eth.ts: -------------------------------------------------------------------------------- 1 | import { StakingClient } from '../../../src/client/staking-client'; 2 | 3 | // Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api 4 | const apiKeyName: string = 'your-api-key-name'; 5 | const apiPrivateKey: string = 'your-api-private-key'; 6 | 7 | const client = new StakingClient(apiKeyName, apiPrivateKey); 8 | 9 | // Defines which partial eth address and rewards we want to see. 10 | const partialETHAddress: string = '0x60c7e246344ae3856cf9abe3a2e258d495fc39e0'; 11 | const partialETHFilter: string = `address='${partialETHAddress}' AND period_end_time > '2024-05-01T00:00:00Z' AND period_end_time < '2024-05-03T00:00:00Z'`; 12 | 13 | // Loops through partial eth rewards array and prints each reward. 14 | (async (): Promise => { 15 | const resp = await client.Ethereum.listRewards(partialETHFilter); 16 | // eslint-disable-next-line @typescript-eslint/padding-line-between-statements 17 | resp.rewards!.forEach((reward) => { 18 | console.log(JSON.stringify(reward, null, 2)); 19 | }); 20 | })(); 21 | -------------------------------------------------------------------------------- /examples/ethereum/list-rewards/validator.ts: -------------------------------------------------------------------------------- 1 | import { StakingClient } from '../../../src/client/staking-client'; 2 | 3 | // Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api 4 | const apiKeyName: string = 'your-api-key-name'; 5 | const apiPrivateKey: string = 'your-api-private-key'; 6 | 7 | const client = new StakingClient(apiKeyName, apiPrivateKey); 8 | 9 | // Defines which validator address and rewards we want to see. 10 | const validatorAddress: string = 11 | '0xac53512c39d0081ca4437c285305eb423f474e6153693c12fbba4a3df78bcaa3422b31d800c5bea71c1b017168a60474'; 12 | const validatorFilter: string = `address='${validatorAddress}' AND period_end_time > '2024-02-25T00:00:00Z' AND period_end_time < '2024-02-27T00:00:00Z'`; 13 | 14 | // Loops through validator rewards array and prints each reward. 15 | (async (): Promise => { 16 | const resp = await client.Ethereum.listRewards(validatorFilter); 17 | // eslint-disable-next-line @typescript-eslint/padding-line-between-statements 18 | resp.rewards!.forEach((reward) => { 19 | console.log(JSON.stringify(reward, null, 2)); 20 | }); 21 | })(); 22 | -------------------------------------------------------------------------------- /examples/ethereum/list-stakes.ts: -------------------------------------------------------------------------------- 1 | import { StakingClient } from '../../src/client/staking-client'; 2 | 3 | // Address can be substituted with any Ethereum validator. 4 | const address: string = 5 | '0xac53512c39d0081ca4437c285305eb423f474e6153693c12fbba4a3df78bcaa3422b31d800c5bea71c1b017168a60474'; 6 | 7 | // Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api 8 | const apiKeyName: string = 'your-api-key-name'; 9 | const apiPrivateKey: string = 'your-api-private-key'; 10 | 11 | const client = new StakingClient(apiKeyName, apiPrivateKey); 12 | 13 | async function listStakes(): Promise { 14 | if (address === '') { 15 | throw new Error('Please set the address variable in this file'); 16 | } 17 | 18 | const filter: string = `address='${address}'`; 19 | 20 | try { 21 | // List ethereum staking balances 22 | let resp = await client.Ethereum.listStakes(filter); 23 | 24 | let count = 0; 25 | 26 | // Loop through staked balance array and print each balance 27 | resp.stakes!.forEach((stake) => { 28 | count++; 29 | const marshaledStake = JSON.stringify(stake); 30 | 31 | console.log(`[${count}] Stake details: ${marshaledStake}`); 32 | }); 33 | } catch (error) { 34 | if (error instanceof Error) { 35 | throw new Error(`Error listing staking balances: ${error.message}`); 36 | } 37 | } 38 | } 39 | 40 | listStakes().catch((error) => { 41 | console.error('Error listing ethereum staking balances: ', error.message); 42 | }); 43 | -------------------------------------------------------------------------------- /examples/example.ts: -------------------------------------------------------------------------------- 1 | import { StakingClient } from '../src/client/staking-client'; 2 | 3 | const client = new StakingClient(); 4 | 5 | client.listProtocols().then((response) => { 6 | console.log(response); 7 | }); 8 | 9 | client.listNetworks('ethereum_kiln').then((response) => { 10 | console.log(response); 11 | }); 12 | 13 | client.listActions('ethereum_kiln', 'holesky').then((response) => { 14 | console.log(response); 15 | }); 16 | -------------------------------------------------------------------------------- /examples/solana/create-and-process-workflow.ts: -------------------------------------------------------------------------------- 1 | import { TxSignerFactory } from '../../src/signers'; 2 | import { 3 | StakingClient, 4 | workflowHasFinished, 5 | workflowWaitingForExternalBroadcast, 6 | isTxStepOutput, 7 | isWaitStepOutput, 8 | } from '../../src/client/staking-client'; 9 | import { Workflow } from '../../src/gen/coinbase/staking/orchestration/v1/workflow.pb'; 10 | import { calculateTimeDifference } from '../../src/utils/date'; 11 | 12 | const walletPrivateKey: string = 'your-wallet-private-key'; // replace with your wallet's private key 13 | const walletAddress: string = 'your-wallet-address'; // replace with your wallet address 14 | const amount: string = '100000000'; // replace with your amount. For solana it should be >= 0.1 SOL 15 | const network: string = 'devnet'; // replace with your network 16 | 17 | // Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api 18 | const apiKeyName: string = 'your-api-key-name'; 19 | const apiPrivateKey: string = 'your-api-private-key'; 20 | 21 | const client = new StakingClient(apiKeyName, apiPrivateKey); 22 | 23 | const signer = TxSignerFactory.getSigner('solana'); 24 | 25 | async function stakeSolana(): Promise { 26 | if (walletPrivateKey === '' || walletAddress === '') { 27 | throw new Error( 28 | 'Please set the walletPrivateKey and walletAddress variables in this file', 29 | ); 30 | } 31 | 32 | let unsignedTx = ''; 33 | let workflow: Workflow = {} as Workflow; 34 | let currentStepId: number | undefined; 35 | 36 | try { 37 | // Create a new solana stake workflow 38 | workflow = await client.Solana.stake(network, walletAddress, amount); 39 | 40 | currentStepId = workflow.currentStepId; 41 | if (currentStepId == null) { 42 | throw new Error('Unexpected workflow state. currentStepId is null'); 43 | } 44 | 45 | console.log('Workflow created %s ...', workflow.name); 46 | } catch (error) { 47 | if (error instanceof Error) { 48 | throw new Error(`Error creating workflow: ${error.message}`); 49 | } 50 | 51 | const msg = JSON.stringify(error); 52 | 53 | throw new Error(`Error creating workflow ${msg}`); 54 | } 55 | 56 | // Loop until the workflow has reached an end state. 57 | // eslint-disable-next-line no-constant-condition 58 | while (true) { 59 | // Every second, get the latest workflow state. 60 | // If the workflow is waiting for signing, sign the unsigned tx and return back the signed tx. 61 | // If the workflow is waiting for external broadcast, sign and broadcast the unsigned tx externally and return back the tx hash via the PerformWorkflowStep API. 62 | // Note: In this example, we just log this message as the wallet provider needs to implement this logic. 63 | try { 64 | workflow = await client.getWorkflow(workflow.name!); 65 | } catch (error) { 66 | // TODO: add retry logic for network errors 67 | if (error instanceof Error) { 68 | throw new Error(`Error getting workflow: ${error.message}`); 69 | } 70 | } 71 | 72 | await printWorkflowProgressDetails(workflow); 73 | 74 | if (workflowWaitingForExternalBroadcast(workflow)) { 75 | unsignedTx = 76 | workflow.steps![currentStepId].txStepOutput?.unsignedTx || ''; 77 | if (unsignedTx === '') { 78 | console.log('Waiting for unsigned tx to be available ...'); 79 | await new Promise((resolve) => setTimeout(resolve, 1000)); // sleep for 1 second 80 | continue; 81 | } 82 | 83 | console.log('Signing unsigned tx %s ...', unsignedTx); 84 | const signedTx = await signer.signTransaction( 85 | walletPrivateKey, 86 | unsignedTx, 87 | ); 88 | 89 | console.log( 90 | 'Please broadcast this signed tx %s externally and return back the tx hash via the PerformWorkflowStep API ...', 91 | signedTx, 92 | ); 93 | break; 94 | } else if (workflowHasFinished(workflow)) { 95 | console.log('Workflow completed with state %s ...', workflow.state); 96 | break; 97 | } 98 | 99 | await new Promise((resolve) => setTimeout(resolve, 1000)); // sleep for 1 second 100 | } 101 | } 102 | 103 | async function printWorkflowProgressDetails(workflow: Workflow): Promise { 104 | if (workflow.steps == null || workflow.steps.length === 0) { 105 | console.log('Waiting for steps to be created ...'); 106 | await new Promise((resolve) => setTimeout(resolve, 1000)); // sleep for 1 second 107 | return; 108 | } 109 | 110 | const currentStepId = workflow.currentStepId; 111 | 112 | if (currentStepId == null) { 113 | return; 114 | } 115 | 116 | const step = workflow.steps[currentStepId]; 117 | 118 | let stepDetails = ''; 119 | 120 | if (isTxStepOutput(step)) { 121 | stepDetails = `state: ${step.txStepOutput?.state} tx hash: ${step.txStepOutput?.txHash}`; 122 | } else if (isWaitStepOutput(step)) { 123 | stepDetails = `state: ${step.waitStepOutput?.state}} current: ${step.waitStepOutput?.current}} target: ${step.waitStepOutput?.target}`; 124 | } else { 125 | throw new Error('Encountered unexpected workflow step type'); 126 | } 127 | 128 | const runtime = calculateTimeDifference( 129 | workflow.createTime, 130 | workflow.updateTime, 131 | ); 132 | 133 | if (workflowHasFinished(workflow)) { 134 | console.log( 135 | 'Workflow reached end state - step name: %s %s workflow state: %s runtime: %d seconds', 136 | step.name, 137 | stepDetails, 138 | workflow.state, 139 | runtime, 140 | ); 141 | } else { 142 | console.log( 143 | 'Waiting for workflow to finish - step name: %s %s workflow state: %s runtime: %d seconds', 144 | step.name, 145 | stepDetails, 146 | workflow.state, 147 | runtime, 148 | ); 149 | } 150 | } 151 | 152 | stakeSolana() 153 | .then(() => { 154 | console.log('Done staking sol'); 155 | }) 156 | .catch((error) => { 157 | console.error('Error staking sol: ', error.message); 158 | }); 159 | -------------------------------------------------------------------------------- /examples/solana/create-workflow.ts: -------------------------------------------------------------------------------- 1 | import { StakingClient } from '../../src/client/staking-client'; 2 | import { Workflow } from '../../src/gen/coinbase/staking/orchestration/v1/workflow.pb'; 3 | 4 | const walletAddress: string = '9NL2SkpcsdyZwsG8NmHGNra4i4NSyKbJTVd9fUQ7kJHR'; // replace with your wallet address 5 | const amount: string = '100000000'; // replace with your amount. For solana it should be >= 0.1 SOL 6 | const network: string = 'mainnet'; // replace with your network 7 | 8 | // Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api 9 | const apiKeyName: string = 'your-api-key-name'; 10 | const apiPrivateKey: string = 'your-api-private-key'; 11 | 12 | const client = new StakingClient(apiKeyName, apiPrivateKey); 13 | 14 | async function stakeSolana(): Promise { 15 | if (walletAddress === '') { 16 | throw new Error('Please set the walletAddress variable in this file'); 17 | } 18 | 19 | let workflow: Workflow = {} as Workflow; 20 | 21 | try { 22 | // Create a new solana stake workflow 23 | workflow = await client.Solana.stake(network, walletAddress, amount); 24 | 25 | console.log(JSON.stringify(workflow, null, 2)); 26 | } catch (error) { 27 | let errorMessage = ''; 28 | 29 | if (error instanceof Error) { 30 | errorMessage = error.message; 31 | } 32 | throw new Error(`Error creating workflow: ${errorMessage}`); 33 | } 34 | } 35 | 36 | stakeSolana() 37 | .then(() => { 38 | console.log('Done creating sol staking workflow'); 39 | }) 40 | .catch((error) => { 41 | if (error instanceof Error) { 42 | console.error('Error creating sol staking workflow: ', error.message); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /examples/solana/list-rewards.ts: -------------------------------------------------------------------------------- 1 | import { StakingClient } from '../../src/client/staking-client'; 2 | 3 | // Address can be substituted with any Solana validator. 4 | const address: string = 'beefKGBWeSpHzYBHZXwp5So7wdQGX6mu4ZHCsH3uTar'; 5 | 6 | // Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api 7 | const apiKeyName: string = 'your-api-key-name'; 8 | const apiPrivateKey: string = 'your-api-private-key'; 9 | 10 | const client = new StakingClient(apiKeyName, apiPrivateKey); 11 | 12 | async function listRewards(): Promise { 13 | if (address === '') { 14 | throw new Error('Please set the address variable in this file'); 15 | } 16 | 17 | const filter: string = `address='${address}'`; 18 | 19 | try { 20 | // List solana rewards 21 | let resp = await client.Solana.listRewards(filter); 22 | 23 | let count = 0; 24 | 25 | // Loop through rewards array and print each reward 26 | resp.rewards!.forEach((reward) => { 27 | count++; 28 | const marshaledReward = JSON.stringify(reward); 29 | 30 | console.log(`[${count}] Reward details: ${marshaledReward}`); 31 | }); 32 | } catch (error) { 33 | if (error instanceof Error) { 34 | throw new Error(`Error listing solana rewards: ${error.message}`); 35 | } 36 | } 37 | } 38 | 39 | listRewards().catch((error) => { 40 | console.error('Error listing solana rewards: ', error.message); 41 | }); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coinbase/staking-client-library-ts", 3 | "version": "0.9.0", 4 | "description": "Coinbase Staking API Typescript Library", 5 | "repository": "https://github.com/coinbase/staking-client-library-ts.git", 6 | "license": "Apache-2.0", 7 | "scripts": { 8 | "clean": "rimraf ./dist", 9 | "gen": "npm run clean-gen && ./scripts/generate-client.sh", 10 | "clean-gen": "rm -rf src/gen/*", 11 | "build": "npm run clean && tsc", 12 | "prepare": "npm run build", 13 | "test": "nyc mocha --require ts-node/register 'src/**/*.test.ts'", 14 | "coverage": "nyc report --reporter=text-lcov && open coverage/index.html", 15 | "lint": "eslint . --ext .ts --ignore-pattern 'dist/**/*' --ignore-pattern 'src/gen/**/*'", 16 | "lint-fix": "eslint . --ext .ts --fix --ignore-pattern 'dist/**/*' --ignore-pattern 'src/gen/**/*'" 17 | }, 18 | "publishConfig": { 19 | "access": "public", 20 | "provenance": true 21 | }, 22 | "files": [ 23 | "src/", 24 | "dist/" 25 | ], 26 | "dependencies": { 27 | "@solana/web3.js": "^1.91.3", 28 | "bs58": "^5.0.0", 29 | "node-jose": "^2.2.0" 30 | }, 31 | "devDependencies": { 32 | "@ethereumjs/tx": "^5.4.0", 33 | "@istanbuljs/nyc-config-typescript": "^1.0.2", 34 | "@types/chai": "^5.0.1", 35 | "@types/mocha": "^10.0.10", 36 | "@types/node": "^20.11.0", 37 | "@types/node-jose": "^1.1.10", 38 | "@types/proxyquire": "^1.3.31", 39 | "@types/sinon": "^17.0.3", 40 | "@typescript-eslint/eslint-plugin": "^6.7.0", 41 | "@typescript-eslint/parser": "^6.7.0", 42 | "chai": "^4.0.0", 43 | "eslint": "^8.57.0", 44 | "eslint-config-prettier": "^9.0.0", 45 | "eslint-plugin-filename-rules": "^1.3.1", 46 | "eslint-plugin-mocha": "^10.5.0", 47 | "eslint-plugin-node": "^11.1.0", 48 | "eslint-plugin-prettier": "^5.1.3", 49 | "eslint-plugin-unicorn": "^51.0.1", 50 | "ethereumjs-util": "^7.1.5", 51 | "mocha": "^8.3.0", 52 | "nyc": "^17.1.0", 53 | "prettier": "^3.0.3", 54 | "prettier-eslint": "^16.0.0", 55 | "proxyquire": "^2.1.3", 56 | "rimraf": "^3.0.2", 57 | "sinon": "^19.0.2", 58 | "ts-node": "^10.9.2", 59 | "typescript": "^5.2.2" 60 | }, 61 | "main": "dist/index.js", 62 | "types": "dist/index.d.ts" 63 | } 64 | -------------------------------------------------------------------------------- /protos/buf.gen.orchestration.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - name: grpc-gateway-ts 4 | out: ./src/gen/ 5 | opt: 6 | - paths=source_relative 7 | - name: openapiv2 8 | out: ./docs/openapi 9 | opt: 10 | - allow_merge=true 11 | - merge_file_name=orchestration 12 | -------------------------------------------------------------------------------- /protos/buf.gen.rewards.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - name: grpc-gateway-ts 4 | out: ./src/gen/ 5 | opt: 6 | - paths=source_relative 7 | - name: openapiv2 8 | out: ./docs/openapi 9 | opt: 10 | - allow_merge=true 11 | - merge_file_name=rewards 12 | - Mcoinbase/staking/rewards/v1/reward_service.proto=github.com/coinbase/staking-client-library-ts 13 | - Mcoinbase/staking/rewards/v1/reward.proto=github.com/coinbase/staking-client-library-ts 14 | - Mcoinbase/staking/rewards/v1/stake.proto=github.com/coinbase/staking-client-library-ts 15 | - Mcoinbase/staking/rewards/v1/common.proto=github.com/coinbase/staking-client-library-ts 16 | - Mcoinbase/staking/rewards/v1/protocol.proto=github.com/coinbase/staking-client-library-ts 17 | - Mcoinbase/staking/rewards/v1/portfolio.proto=github.com/coinbase/staking-client-library-ts 18 | -------------------------------------------------------------------------------- /scripts/generate-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | buf generate --template protos/buf.gen.orchestration.yaml buf.build/cdp/orchestration --path coinbase/staking/orchestration/v1 --include-imports --include-wkt 4 | buf generate --template protos/buf.gen.rewards.yaml buf.build/cdp/rewards --path coinbase/staking/rewards/v1 --include-imports --include-wkt 5 | # TODO: Remove this once the generation issue is fixed. 6 | find ./src/gen -type f -exec sed -I '' -e 's/parentprotocols/parent/g' -e 's/parentprotocolsnetworks/parents/g' -e 's/parentnetworks/parent/g' -e 's/nameprojectsworkflows/name/g' -e 's/nameworkflows/name/g' {} \; 7 | -------------------------------------------------------------------------------- /src/auth/jwt.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import proxyquire from 'proxyquire'; 4 | import { JWK, JWS } from 'node-jose'; 5 | 6 | describe('BuildJWT', () => { 7 | let readFileSyncStub: sinon.SinonStub; 8 | let asKeyStub: sinon.SinonStub; 9 | let signStub: sinon.SinonStub; 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | let buildJWT: any; 12 | 13 | beforeEach(() => { 14 | readFileSyncStub = sinon.stub(); 15 | asKeyStub = sinon.stub(JWK, 'asKey'); 16 | signStub = sinon.stub(JWS, 'createSign'); 17 | 18 | buildJWT = proxyquire('./jwt', { 19 | fs: { readFileSync: readFileSyncStub }, 20 | }).buildJWT; 21 | }); 22 | 23 | afterEach(() => { 24 | sinon.restore(); 25 | }); 26 | 27 | it('should build a JWT with provided API key and private key', async () => { 28 | const url = 'https://api.example.com/resource'; 29 | const method = 'POST'; 30 | const apiKeyName = 'test-api-key'; 31 | const apiPrivateKey = 32 | '-----BEGIN EC PRIVATE KEY-----\ntest-private-key\n-----END EC PRIVATE KEY-----'; 33 | const pemPrivateKey = 34 | '-----BEGIN EC PRIVATE KEY-----test-private-key-----END EC PRIVATE KEY-----'; 35 | const privateKey = { kty: 'EC' }; 36 | 37 | asKeyStub.resolves(privateKey); 38 | signStub.returns({ 39 | update: sinon.stub().returnsThis(), 40 | final: sinon.stub().resolves('signed-jwt'), 41 | }); 42 | 43 | const jwt = await buildJWT(url, method, apiKeyName, apiPrivateKey); 44 | 45 | expect(asKeyStub.calledOnceWithExactly(pemPrivateKey, 'pem')).to.be.true; 46 | expect(signStub.calledOnce).to.be.true; 47 | expect(jwt).to.equal('signed-jwt'); 48 | }); 49 | 50 | it('should build a JWT with API key from file', async () => { 51 | const url = 'https://api.example.com/resource'; 52 | const method = 'POST'; 53 | const apiKey = { 54 | name: 'test-api-key', 55 | privateKey: 56 | '-----BEGIN EC PRIVATE KEY-----test-private-key-----END EC PRIVATE KEY-----', 57 | }; 58 | const pemPrivateKey = 59 | '-----BEGIN EC PRIVATE KEY-----test-private-key-----END EC PRIVATE KEY-----'; 60 | const privateKey = { kty: 'EC' }; 61 | 62 | readFileSyncStub.returns(JSON.stringify(apiKey)); 63 | asKeyStub.resolves(privateKey); 64 | signStub.returns({ 65 | update: sinon.stub().returnsThis(), 66 | final: sinon.stub().resolves('signed-jwt'), 67 | }); 68 | 69 | const jwt = await buildJWT(url, method); 70 | 71 | expect( 72 | readFileSyncStub.calledOnceWithExactly('.coinbase_cloud_api_key.json', { 73 | encoding: 'utf8', 74 | }), 75 | ).to.be.true; 76 | expect(asKeyStub.calledOnceWithExactly(pemPrivateKey, 'pem')).to.be.true; 77 | expect(signStub.calledOnce).to.be.true; 78 | expect(jwt).to.equal('signed-jwt'); 79 | }); 80 | 81 | it('should throw an error if private key is not EC', async () => { 82 | const url = 'https://api.example.com/resource'; 83 | const method = 'POST'; 84 | const apiKeyName = 'test-api-key'; 85 | const apiPrivateKey = 86 | '-----BEGIN EC PRIVATE KEY-----\ntest-private-key\n-----END EC PRIVATE KEY-----'; 87 | const pemPrivateKey = 88 | '-----BEGIN EC PRIVATE KEY-----test-private-key-----END EC PRIVATE KEY-----'; 89 | const privateKey = { kty: 'RSA' }; 90 | 91 | asKeyStub.resolves(privateKey); 92 | 93 | try { 94 | await buildJWT(url, method, apiKeyName, apiPrivateKey); 95 | expect.fail('Expected buildJWT to throw an error'); 96 | } catch (error) { 97 | expect((error as Error).message).to.contain('Not an EC private key'); 98 | } 99 | 100 | expect(asKeyStub.calledOnceWithExactly(pemPrivateKey, 'pem')).to.be.true; 101 | expect(signStub.notCalled).to.be.true; 102 | }); 103 | 104 | it('should throw an error if private key cannot be parsed', async () => { 105 | const url = 'https://api.example.com/resource'; 106 | const method = 'POST'; 107 | const apiKeyName = 'test-api-key'; 108 | const apiPrivateKey = 109 | '-----BEGIN EC PRIVATE KEY-----\ntest-private-key\n-----END EC PRIVATE KEY-----'; 110 | const pemPrivateKey = 111 | '-----BEGIN EC PRIVATE KEY-----test-private-key-----END EC PRIVATE KEY-----'; 112 | 113 | asKeyStub.rejects(new Error('Invalid key')); 114 | 115 | try { 116 | await buildJWT(url, method, apiKeyName, apiPrivateKey); 117 | expect.fail('Expected buildJWT to throw an error'); 118 | } catch (error) { 119 | expect((error as Error).message).to.include( 120 | 'jwt: Could not decode or parse private key. Error: Invalid key', 121 | ); 122 | } 123 | 124 | expect(asKeyStub.calledOnceWithExactly(pemPrivateKey, 'pem')).to.be.true; 125 | expect(signStub.notCalled).to.be.true; 126 | }); 127 | 128 | it('should throw an error if JWT signing fails', async () => { 129 | const url = 'https://api.example.com/resource'; 130 | const method = 'POST'; 131 | const apiKeyName = 'test-api-key'; 132 | const apiPrivateKey = 133 | '-----BEGIN EC PRIVATE KEY-----\ntest-private-key\n-----END EC PRIVATE KEY-----'; 134 | const pemPrivateKey = 135 | '-----BEGIN EC PRIVATE KEY-----test-private-key-----END EC PRIVATE KEY-----'; 136 | const privateKey = { kty: 'EC' }; 137 | 138 | asKeyStub.resolves(privateKey); 139 | signStub.returns({ 140 | update: sinon.stub().returnsThis(), 141 | final: sinon.stub().rejects(new Error('Signing failed')), 142 | }); 143 | 144 | try { 145 | await buildJWT(url, method, apiKeyName, apiPrivateKey); 146 | expect.fail('Expected buildJWT to throw an error'); 147 | } catch (error) { 148 | expect((error as Error).message).to.include( 149 | 'jwt: Failed to sign JWT. Error: Signing failed', 150 | ); 151 | } 152 | 153 | expect(asKeyStub.calledOnceWithExactly(pemPrivateKey, 'pem')).to.be.true; 154 | expect(signStub.calledOnce).to.be.true; 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /src/auth/jwt.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { JWK, JWS } from 'node-jose'; 3 | 4 | const legacyPemHeader = '-----BEGIN ECDSA Private Key-----'; 5 | const legacyPemFooter = '-----END ECDSA Private Key-----'; 6 | const pemHeader = '-----BEGIN EC PRIVATE KEY-----'; 7 | const pemFooter = '-----END EC PRIVATE KEY-----'; 8 | 9 | /** 10 | * Build a JWT for the specified service and URI. 11 | * @returns The generated JWT. 12 | * @param url The URL for which the JWT is to be generated. 13 | * @param method The HTTP method for the request. 14 | * @param apiKeyName The name of the API key. 15 | * @param apiPrivateKey The private key present in the API key downloaded from platform. 16 | */ 17 | export const buildJWT = async ( 18 | url: string, 19 | method = 'GET', 20 | apiKeyName?: string, 21 | apiPrivateKey?: string, 22 | ): Promise => { 23 | let pemPrivateKey: string; 24 | let keyName: string; 25 | let apiKey: APIKey; 26 | 27 | if (apiKeyName && apiPrivateKey) { 28 | pemPrivateKey = extractPemKey(apiPrivateKey); 29 | keyName = apiKeyName; 30 | } else { 31 | const keyFile = readFileSync('.coinbase_cloud_api_key.json', { 32 | encoding: 'utf8', 33 | }); 34 | 35 | apiKey = JSON.parse(keyFile); 36 | pemPrivateKey = extractPemKey(apiKey.privateKey); 37 | keyName = apiKey.name; 38 | } 39 | 40 | let privateKey: JWK.Key; 41 | 42 | try { 43 | privateKey = await JWK.asKey(pemPrivateKey, 'pem'); 44 | if (privateKey.kty !== 'EC') { 45 | throw new Error('Not an EC private key'); 46 | } 47 | } catch (error) { 48 | throw new Error(`jwt: Could not decode or parse private key. ${error}`); 49 | } 50 | 51 | const header = { 52 | alg: 'ES256', 53 | kid: keyName, 54 | typ: 'JWT', 55 | nonce: nonce(), 56 | }; 57 | 58 | const audience = getAudience(url); 59 | const uri = `${method} ${url.substring(8)}`; 60 | 61 | const claims: APIKeyClaims = { 62 | sub: keyName, 63 | iss: 'coinbase-cloud', 64 | nbf: Math.floor(Date.now() / 1000), 65 | exp: Math.floor(Date.now() / 1000) + 60, // +1 minute 66 | aud: [audience], 67 | uri, 68 | }; 69 | 70 | const payload = Buffer.from(JSON.stringify(claims)).toString('utf8'); 71 | 72 | try { 73 | const result = await JWS.createSign( 74 | { format: 'compact', fields: header }, 75 | privateKey, 76 | ) 77 | .update(payload) 78 | .final(); 79 | 80 | return result as unknown as string; 81 | } catch (err) { 82 | throw new Error(`jwt: Failed to sign JWT. ${err}`); 83 | } 84 | }; 85 | 86 | /** 87 | * Represents the API key details. 88 | */ 89 | interface APIKey { 90 | /** The name of the API key. */ 91 | name: string; 92 | /** The private key string. */ 93 | privateKey: string; 94 | } 95 | 96 | /** 97 | * Represents the claims included in the JWT. 98 | */ 99 | interface APIKeyClaims { 100 | /** Audience of the JWT. */ 101 | aud: string[]; 102 | /** Subject of the JWT. Typically the identifier of the API key. */ 103 | sub: string; 104 | /** Expiry time of the JWT in seconds. */ 105 | exp: number; 106 | /** Time before which the JWT is not valid in seconds. */ 107 | nbf: number; 108 | /** Issuer of the JWT. */ 109 | iss: string; 110 | /** URI claim for the JWT. */ 111 | uri: string; 112 | } 113 | 114 | /** 115 | * Extracts the PEM key from the given string. 116 | * @param privateKeyString The string for the private key from which to extract the PEM key. 117 | * @returns The extracted PEM key body. 118 | */ 119 | const extractPemKey = (privateKeyString: string): string => { 120 | // Remove all newline characters 121 | privateKeyString = privateKeyString.replace(/\\n|\n/g, ''); 122 | 123 | // If the string starts with the standard PEM header and footer, return as is. 124 | if ( 125 | privateKeyString.startsWith(pemHeader) && 126 | privateKeyString.endsWith(pemFooter) 127 | ) { 128 | return privateKeyString; 129 | } 130 | 131 | // If the string starts with the legacy header and footer, replace them. 132 | const regex = new RegExp( 133 | `^${legacyPemHeader}([\\s\\S]+?)${legacyPemFooter}$`, 134 | ); 135 | 136 | const match = privateKeyString.match(regex); 137 | 138 | if (match && match[1]) { 139 | return pemHeader + match[1].trim() + pemFooter; 140 | } 141 | 142 | // The string does not match any of the expected formats. 143 | throw new Error('wrong format of API private key'); 144 | }; 145 | 146 | /** 147 | * Generates a nonce of 16 numeric characters. 148 | * @returns The generated nonce. 149 | */ 150 | const nonce = (): string => { 151 | const range = '0123456789'; 152 | let result = ''; 153 | 154 | for (let i = 0; i < 16; i++) { 155 | result += range.charAt(Math.floor(Math.random() * range.length)); 156 | } 157 | 158 | return result; 159 | }; 160 | 161 | const getAudience = (url: string): string => { 162 | if (url.indexOf('staking') > -1) { 163 | return 'staking'; 164 | } else if (url.indexOf('rewards') > -1) { 165 | return 'rewards-reporting'; 166 | } else { 167 | return 'unknown'; 168 | } 169 | }; 170 | 171 | export const customFetch = async ( 172 | input: RequestInfo | URL, 173 | init?: RequestInit | undefined, 174 | ): Promise => { 175 | // remove query parameters 176 | let url = input.toString(); 177 | 178 | if (url.indexOf('?') > -1) { 179 | url = url.substring(0, url.indexOf('?')); 180 | } 181 | const token = await buildJWT(url, init?.method); 182 | const params = { 183 | ...init, 184 | headers: { 185 | ...init?.headers, 186 | Authorization: `Bearer ${token}`, 187 | }, 188 | }; 189 | 190 | return fetch(input, params); 191 | }; 192 | -------------------------------------------------------------------------------- /src/client/protocols/ethereum-kiln-staking.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import { Ethereum } from './ethereum-kiln-staking'; 4 | import { StakingClient } from '../staking-client'; 5 | import { 6 | CreateWorkflowRequest, 7 | Workflow, 8 | } from '../../gen/coinbase/staking/orchestration/v1/workflow.pb'; 9 | import { 10 | ViewStakingContextRequest, 11 | ViewStakingContextResponse, 12 | } from '../../gen/coinbase/staking/orchestration/v1/staking_context.pb'; 13 | import { 14 | ListRewardsRequest, 15 | ListRewardsResponse, 16 | } from '../../gen/coinbase/staking/rewards/v1/reward.pb'; 17 | import { 18 | ListStakesRequest, 19 | ListStakesResponse, 20 | } from '../../gen/coinbase/staking/rewards/v1/stake.pb'; 21 | 22 | describe('Ethereum', () => { 23 | let stakingClientStub: sinon.SinonStubbedInstance; 24 | let ethereum: Ethereum; 25 | 26 | beforeEach(() => { 27 | stakingClientStub = sinon.createStubInstance(StakingClient); 28 | ethereum = new Ethereum(stakingClientStub); 29 | }); 30 | 31 | afterEach(() => { 32 | sinon.restore(); 33 | }); 34 | 35 | describe('stake', () => { 36 | it('should stake with the correct parameters', async () => { 37 | const network = 'mainnet'; 38 | const stakerAddress = 'some-staker-address'; 39 | const amount = '100'; 40 | const integratorContractAddress = 'some-integrator-contract-address'; 41 | const req: CreateWorkflowRequest = { 42 | workflow: { 43 | action: `protocols/ethereum_kiln/networks/${network}/actions/stake`, 44 | ethereumKilnStakingParameters: { 45 | stakeParameters: { 46 | stakerAddress: stakerAddress, 47 | integratorContractAddress: integratorContractAddress, 48 | amount: { 49 | value: amount, 50 | currency: 'ETH', 51 | }, 52 | }, 53 | }, 54 | }, 55 | }; 56 | const res: Workflow = { 57 | /* response data */ 58 | }; 59 | 60 | stakingClientStub.createWorkflow.resolves(res); 61 | 62 | const response = await ethereum.stake( 63 | network, 64 | stakerAddress, 65 | amount, 66 | integratorContractAddress, 67 | ); 68 | 69 | expect(stakingClientStub.createWorkflow.calledOnceWithExactly(req)).to.be 70 | .true; 71 | expect(response).to.deep.equal(res); 72 | }); 73 | 74 | it('should handle missing integrator contract address', async () => { 75 | const network = 'mainnet'; 76 | const stakerAddress = 'some-staker-address'; 77 | const amount = '100'; 78 | const req: CreateWorkflowRequest = { 79 | workflow: { 80 | action: `protocols/ethereum_kiln/networks/${network}/actions/stake`, 81 | ethereumKilnStakingParameters: { 82 | stakeParameters: { 83 | stakerAddress: stakerAddress, 84 | integratorContractAddress: undefined, 85 | amount: { 86 | value: amount, 87 | currency: 'ETH', 88 | }, 89 | }, 90 | }, 91 | }, 92 | }; 93 | const res: Workflow = { 94 | /* response data */ 95 | }; 96 | 97 | stakingClientStub.createWorkflow.resolves(res); 98 | 99 | const response = await ethereum.stake(network, stakerAddress, amount); 100 | 101 | expect(stakingClientStub.createWorkflow.calledOnceWithExactly(req)).to.be 102 | .true; 103 | expect(response).to.deep.equal(res); 104 | }); 105 | 106 | it('should handle invalid amount', async () => { 107 | const network = 'mainnet'; 108 | const stakerAddress = 'some-staker-address'; 109 | const amount = '-100'; // Invalid amount 110 | const req: CreateWorkflowRequest = { 111 | workflow: { 112 | action: `protocols/ethereum_kiln/networks/${network}/actions/stake`, 113 | ethereumKilnStakingParameters: { 114 | stakeParameters: { 115 | stakerAddress: stakerAddress, 116 | integratorContractAddress: undefined, 117 | amount: { 118 | value: amount, 119 | currency: 'ETH', 120 | }, 121 | }, 122 | }, 123 | }, 124 | }; 125 | const res: Workflow = { 126 | /* response data */ 127 | }; 128 | 129 | stakingClientStub.createWorkflow.resolves(res); 130 | 131 | const response = await ethereum.stake(network, stakerAddress, amount); 132 | 133 | expect(stakingClientStub.createWorkflow.calledOnceWithExactly(req)).to.be 134 | .true; 135 | expect(response).to.deep.equal(res); 136 | }); 137 | }); 138 | 139 | describe('unstake', () => { 140 | it('should unstake with the correct parameters, empty integrator contract address', async () => { 141 | const network = 'mainnet'; 142 | const stakerAddress = 'some-staker-address'; 143 | 144 | const amount = '100'; 145 | const req: CreateWorkflowRequest = { 146 | workflow: { 147 | action: `protocols/ethereum_kiln/networks/${network}/actions/unstake`, 148 | ethereumKilnStakingParameters: { 149 | unstakeParameters: { 150 | integratorContractAddress: undefined, 151 | stakerAddress: stakerAddress, 152 | amount: { 153 | value: amount, 154 | currency: 'ETH', 155 | }, 156 | }, 157 | }, 158 | }, 159 | }; 160 | const res: Workflow = { 161 | /* response data */ 162 | }; 163 | 164 | stakingClientStub.createWorkflow.resolves(res); 165 | 166 | const response = await ethereum.unstake(network, stakerAddress, amount); 167 | 168 | expect(stakingClientStub.createWorkflow.calledOnceWithExactly(req)).to.be 169 | .true; 170 | expect(response).to.deep.equal(res); 171 | }); 172 | 173 | it('should unstake with the correct parameters, with integrator contract address', async () => { 174 | const network = 'mainnet'; 175 | const stakerAddress = 'some-staker-address'; 176 | const integratorContractAddress = 'some-integrator-contract-address'; 177 | const amount = '100'; 178 | const req: CreateWorkflowRequest = { 179 | workflow: { 180 | action: `protocols/ethereum_kiln/networks/${network}/actions/unstake`, 181 | ethereumKilnStakingParameters: { 182 | unstakeParameters: { 183 | integratorContractAddress: integratorContractAddress, 184 | stakerAddress: stakerAddress, 185 | amount: { 186 | value: amount, 187 | currency: 'ETH', 188 | }, 189 | }, 190 | }, 191 | }, 192 | }; 193 | const res: Workflow = { 194 | /* response data */ 195 | }; 196 | 197 | stakingClientStub.createWorkflow.resolves(res); 198 | 199 | const response = await ethereum.unstake( 200 | network, 201 | stakerAddress, 202 | amount, 203 | integratorContractAddress, 204 | ); 205 | 206 | expect(stakingClientStub.createWorkflow.calledOnceWithExactly(req)).to.be 207 | .true; 208 | expect(response).to.deep.equal(res); 209 | }); 210 | }); 211 | 212 | describe('viewStakingContext', () => { 213 | it('should view staking context with the correct parameters', async () => { 214 | const address = 'some-address'; 215 | const integratorContractAddress = 'some-integrator-contract-address'; 216 | const network = 'mainnet'; 217 | const req: ViewStakingContextRequest = { 218 | address: address, 219 | network: `protocols/ethereum_kiln/networks/${network}`, 220 | ethereumKilnStakingContextParameters: { 221 | integratorContractAddress: integratorContractAddress, 222 | }, 223 | }; 224 | const res: ViewStakingContextResponse = { 225 | /* response data */ 226 | }; 227 | 228 | stakingClientStub.viewStakingContext.resolves(res); 229 | 230 | const response = await ethereum.viewStakingContext( 231 | address, 232 | network, 233 | integratorContractAddress, 234 | ); 235 | 236 | expect(stakingClientStub.viewStakingContext.calledOnceWithExactly(req)).to 237 | .be.true; 238 | expect(response).to.deep.equal(res); 239 | }); 240 | }); 241 | 242 | describe('listRewards', () => { 243 | it('should list rewards with the correct parameters', async () => { 244 | const filter = 'some-filter'; 245 | const pageSize = 50; 246 | const pageToken = 'some-token'; 247 | const req: ListRewardsRequest = { 248 | parent: 'protocols/ethereum', 249 | filter: filter, 250 | pageSize: pageSize, 251 | pageToken: pageToken, 252 | }; 253 | const res: ListRewardsResponse = { 254 | /* response data */ 255 | }; 256 | 257 | stakingClientStub.listRewards.resolves(res); 258 | 259 | const response = await ethereum.listRewards(filter, pageSize, pageToken); 260 | 261 | expect( 262 | stakingClientStub.listRewards.calledOnceWithExactly('ethereum', req), 263 | ).to.be.true; 264 | expect(response).to.deep.equal(res); 265 | }); 266 | 267 | it('should use default parameters when not provided', async () => { 268 | const filter = 'some-filter'; 269 | const req: ListRewardsRequest = { 270 | parent: 'protocols/ethereum', 271 | filter: filter, 272 | pageSize: 100, 273 | pageToken: undefined, 274 | }; 275 | const res: ListRewardsResponse = { 276 | /* response data */ 277 | }; 278 | 279 | stakingClientStub.listRewards.resolves(res); 280 | 281 | const response = await ethereum.listRewards(filter); 282 | 283 | expect( 284 | stakingClientStub.listRewards.calledOnceWithExactly('ethereum', req), 285 | ).to.be.true; 286 | expect(response).to.deep.equal(res); 287 | }); 288 | 289 | it('should handle empty filter', async () => { 290 | const filter = ''; 291 | const req: ListRewardsRequest = { 292 | parent: 'protocols/ethereum', 293 | filter: filter, 294 | pageSize: 100, 295 | pageToken: undefined, 296 | }; 297 | const res: ListRewardsResponse = { 298 | /* response data */ 299 | }; 300 | 301 | stakingClientStub.listRewards.resolves(res); 302 | 303 | const response = await ethereum.listRewards(filter); 304 | 305 | expect( 306 | stakingClientStub.listRewards.calledOnceWithExactly('ethereum', req), 307 | ).to.be.true; 308 | expect(response).to.deep.equal(res); 309 | }); 310 | }); 311 | 312 | describe('listStakes', () => { 313 | it('should list stakes with the correct parameters', async () => { 314 | const filter = 'some-filter'; 315 | const pageSize = 50; 316 | const pageToken = 'some-token'; 317 | const req: ListStakesRequest = { 318 | parent: 'protocols/ethereum', 319 | filter: filter, 320 | pageSize: pageSize, 321 | pageToken: pageToken, 322 | }; 323 | const res: ListStakesResponse = { 324 | /* response data */ 325 | }; 326 | 327 | stakingClientStub.listStakes.resolves(res); 328 | 329 | const response = await ethereum.listStakes(filter, pageSize, pageToken); 330 | 331 | expect( 332 | stakingClientStub.listStakes.calledOnceWithExactly('ethereum', req), 333 | ).to.be.true; 334 | expect(response).to.deep.equal(res); 335 | }); 336 | 337 | it('should use default parameters when not provided', async () => { 338 | const filter = 'some-filter'; 339 | const req: ListStakesRequest = { 340 | parent: 'protocols/ethereum', 341 | filter: filter, 342 | pageSize: 100, 343 | pageToken: undefined, 344 | }; 345 | const res: ListStakesResponse = { 346 | /* response data */ 347 | }; 348 | 349 | stakingClientStub.listStakes.resolves(res); 350 | 351 | const response = await ethereum.listStakes(filter); 352 | 353 | expect( 354 | stakingClientStub.listStakes.calledOnceWithExactly('ethereum', req), 355 | ).to.be.true; 356 | expect(response).to.deep.equal(res); 357 | }); 358 | 359 | it('should handle empty filter', async () => { 360 | const filter = ''; 361 | const req: ListStakesRequest = { 362 | parent: 'protocols/ethereum', 363 | filter: filter, 364 | pageSize: 100, 365 | pageToken: undefined, 366 | }; 367 | const res: ListStakesResponse = { 368 | /* response data */ 369 | }; 370 | 371 | stakingClientStub.listStakes.resolves(res); 372 | 373 | const response = await ethereum.listStakes(filter); 374 | 375 | expect( 376 | stakingClientStub.listStakes.calledOnceWithExactly('ethereum', req), 377 | ).to.be.true; 378 | expect(response).to.deep.equal(res); 379 | }); 380 | }); 381 | }); 382 | -------------------------------------------------------------------------------- /src/client/protocols/ethereum-kiln-staking.ts: -------------------------------------------------------------------------------- 1 | import { StakingClient } from '../staking-client'; 2 | import { 3 | CreateWorkflowRequest, 4 | Workflow, 5 | } from '../../gen/coinbase/staking/orchestration/v1/workflow.pb'; 6 | import { 7 | ViewStakingContextRequest, 8 | ViewStakingContextResponse, 9 | } from '../../gen/coinbase/staking/orchestration/v1/staking_context.pb'; 10 | import { 11 | ListRewardsRequest, 12 | ListRewardsResponse, 13 | } from '../../gen/coinbase/staking/rewards/v1/reward.pb'; 14 | import { 15 | ListStakesRequest, 16 | ListStakesResponse, 17 | } from '../../gen/coinbase/staking/rewards/v1/stake.pb'; 18 | 19 | export class Ethereum { 20 | private parent: StakingClient; 21 | 22 | constructor(parent: StakingClient) { 23 | this.parent = parent; 24 | } 25 | 26 | async stake( 27 | network: string, 28 | stakerAddress: string, 29 | amount: string, 30 | integratorContractAddress?: string, 31 | ): Promise { 32 | const req: CreateWorkflowRequest = { 33 | workflow: { 34 | action: `protocols/ethereum_kiln/networks/${network}/actions/stake`, 35 | ethereumKilnStakingParameters: { 36 | stakeParameters: { 37 | stakerAddress: stakerAddress, 38 | integratorContractAddress: integratorContractAddress, 39 | amount: { 40 | value: amount, 41 | currency: 'ETH', 42 | }, 43 | }, 44 | }, 45 | }, 46 | }; 47 | 48 | return this.parent.createWorkflow(req); 49 | } 50 | 51 | async unstake( 52 | network: string, 53 | stakerAddress: string, 54 | amount: string, 55 | integratorContractAddress?: string, 56 | ): Promise { 57 | const req: CreateWorkflowRequest = { 58 | workflow: { 59 | action: `protocols/ethereum_kiln/networks/${network}/actions/unstake`, 60 | ethereumKilnStakingParameters: { 61 | unstakeParameters: { 62 | stakerAddress: stakerAddress, 63 | integratorContractAddress: integratorContractAddress, 64 | amount: { 65 | value: amount, 66 | currency: 'ETH', 67 | }, 68 | }, 69 | }, 70 | }, 71 | }; 72 | 73 | return this.parent.createWorkflow(req); 74 | } 75 | 76 | async claimStake( 77 | network: string, 78 | stakerAddress: string, 79 | integratorContractAddress?: string, 80 | ): Promise { 81 | const req: CreateWorkflowRequest = { 82 | workflow: { 83 | action: `protocols/ethereum_kiln/networks/${network}/actions/claim_stake`, 84 | ethereumKilnStakingParameters: { 85 | claimStakeParameters: { 86 | stakerAddress: stakerAddress, 87 | integratorContractAddress: integratorContractAddress, 88 | }, 89 | }, 90 | }, 91 | }; 92 | 93 | return this.parent.createWorkflow(req); 94 | } 95 | 96 | async viewStakingContext( 97 | address: string, 98 | network: string, 99 | integratorContractAddress: string, 100 | ): Promise { 101 | const req: ViewStakingContextRequest = { 102 | address: address, 103 | network: `protocols/ethereum_kiln/networks/${network}`, 104 | ethereumKilnStakingContextParameters: { 105 | integratorContractAddress: integratorContractAddress, 106 | }, 107 | }; 108 | 109 | return this.parent.viewStakingContext(req); 110 | } 111 | 112 | async listRewards( 113 | filter: string, 114 | pageSize: number = 100, 115 | pageToken?: string, 116 | ): Promise { 117 | const req: ListRewardsRequest = { 118 | parent: 'protocols/ethereum', 119 | filter: filter, 120 | pageSize: pageSize, 121 | pageToken: pageToken, 122 | }; 123 | 124 | return this.parent.listRewards('ethereum', req); 125 | } 126 | 127 | async listStakes( 128 | filter: string, 129 | pageSize: number = 100, 130 | pageToken?: string, 131 | ): Promise { 132 | const req: ListStakesRequest = { 133 | parent: 'protocols/ethereum', 134 | filter: filter, 135 | pageSize: pageSize, 136 | pageToken: pageToken, 137 | }; 138 | 139 | return this.parent.listStakes('ethereum', req); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/client/protocols/solana-staking.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import { Solana } from './solana-staking'; 4 | import { StakingClient } from '../staking-client'; 5 | import { 6 | CreateWorkflowRequest, 7 | Workflow, 8 | } from '../../gen/coinbase/staking/orchestration/v1/workflow.pb'; 9 | import { 10 | ViewStakingContextRequest, 11 | ViewStakingContextResponse, 12 | } from '../../gen/coinbase/staking/orchestration/v1/staking_context.pb'; 13 | import { 14 | ListRewardsRequest, 15 | ListRewardsResponse, 16 | } from '../../gen/coinbase/staking/rewards/v1/reward.pb'; 17 | import { 18 | ListStakesRequest, 19 | ListStakesResponse, 20 | } from '../../gen/coinbase/staking/rewards/v1/stake.pb'; 21 | 22 | describe('Solana', () => { 23 | let stakingClientStub: sinon.SinonStubbedInstance; 24 | let solana: Solana; 25 | 26 | beforeEach(() => { 27 | stakingClientStub = sinon.createStubInstance(StakingClient); 28 | solana = new Solana(stakingClientStub); 29 | }); 30 | 31 | afterEach(() => { 32 | sinon.restore(); 33 | }); 34 | 35 | describe('viewStakingContext', () => { 36 | it('should view staking context with the correct parameters', async () => { 37 | const address = 'some-address'; 38 | const network = 'mainnet'; 39 | const req: ViewStakingContextRequest = { 40 | address: address, 41 | network: `protocols/solana/networks/${network}`, 42 | }; 43 | const res: ViewStakingContextResponse = { 44 | /* response data */ 45 | }; 46 | 47 | stakingClientStub.viewStakingContext.resolves(res); 48 | 49 | const response = await solana.viewStakingContext(address, network); 50 | 51 | expect(stakingClientStub.viewStakingContext.calledOnceWithExactly(req)).to 52 | .be.true; 53 | expect(response).to.deep.equal(res); 54 | }); 55 | }); 56 | 57 | describe('listRewards', () => { 58 | it('should list rewards with the correct parameters', async () => { 59 | const filter = 'some-filter'; 60 | const pageSize = 50; 61 | const pageToken = 'some-token'; 62 | const req: ListRewardsRequest = { 63 | parent: 'protocols/solana', 64 | filter: filter, 65 | pageSize: pageSize, 66 | pageToken: pageToken, 67 | }; 68 | const res: ListRewardsResponse = { 69 | /* response data */ 70 | }; 71 | 72 | stakingClientStub.listRewards.resolves(res); 73 | 74 | const response = await solana.listRewards(filter, pageSize, pageToken); 75 | 76 | expect(stakingClientStub.listRewards.calledOnceWithExactly('solana', req)) 77 | .to.be.true; 78 | expect(response).to.deep.equal(res); 79 | }); 80 | 81 | it('should use default parameters when not provided', async () => { 82 | const filter = 'some-filter'; 83 | const req: ListRewardsRequest = { 84 | parent: 'protocols/solana', 85 | filter: filter, 86 | pageSize: 100, 87 | pageToken: undefined, 88 | }; 89 | const res: ListRewardsResponse = { 90 | /* response data */ 91 | }; 92 | 93 | stakingClientStub.listRewards.resolves(res); 94 | 95 | const response = await solana.listRewards(filter); 96 | 97 | expect(stakingClientStub.listRewards.calledOnceWithExactly('solana', req)) 98 | .to.be.true; 99 | expect(response).to.deep.equal(res); 100 | }); 101 | 102 | it('should handle empty filter', async () => { 103 | const filter = ''; 104 | const req: ListRewardsRequest = { 105 | parent: 'protocols/solana', 106 | filter: filter, 107 | pageSize: 100, 108 | pageToken: undefined, 109 | }; 110 | const res: ListRewardsResponse = { 111 | /* response data */ 112 | }; 113 | 114 | stakingClientStub.listRewards.resolves(res); 115 | 116 | const response = await solana.listRewards(filter); 117 | 118 | expect(stakingClientStub.listRewards.calledOnceWithExactly('solana', req)) 119 | .to.be.true; 120 | expect(response).to.deep.equal(res); 121 | }); 122 | }); 123 | 124 | describe('listStakes', () => { 125 | it('should list stakes with the correct parameters', async () => { 126 | const filter = 'some-filter'; 127 | const pageSize = 50; 128 | const pageToken = 'some-token'; 129 | const req: ListStakesRequest = { 130 | parent: 'protocols/solana', 131 | filter: filter, 132 | pageSize: pageSize, 133 | pageToken: pageToken, 134 | }; 135 | const res: ListStakesResponse = { 136 | /* response data */ 137 | }; 138 | 139 | stakingClientStub.listStakes.resolves(res); 140 | 141 | const response = await solana.listStakes(filter, pageSize, pageToken); 142 | 143 | expect(stakingClientStub.listStakes.calledOnceWithExactly('solana', req)) 144 | .to.be.true; 145 | expect(response).to.deep.equal(res); 146 | }); 147 | 148 | it('should use default parameters when not provided', async () => { 149 | const filter = 'some-filter'; 150 | const req: ListStakesRequest = { 151 | parent: 'protocols/solana', 152 | filter: filter, 153 | pageSize: 100, 154 | pageToken: undefined, 155 | }; 156 | const res: ListStakesResponse = { 157 | /* response data */ 158 | }; 159 | 160 | stakingClientStub.listStakes.resolves(res); 161 | 162 | const response = await solana.listStakes(filter); 163 | 164 | expect(stakingClientStub.listStakes.calledOnceWithExactly('solana', req)) 165 | .to.be.true; 166 | expect(response).to.deep.equal(res); 167 | }); 168 | 169 | it('should handle empty filter', async () => { 170 | const filter = ''; 171 | const req: ListStakesRequest = { 172 | parent: 'protocols/solana', 173 | filter: filter, 174 | pageSize: 100, 175 | pageToken: undefined, 176 | }; 177 | const res: ListStakesResponse = { 178 | /* response data */ 179 | }; 180 | 181 | stakingClientStub.listStakes.resolves(res); 182 | 183 | const response = await solana.listStakes(filter); 184 | 185 | expect(stakingClientStub.listStakes.calledOnceWithExactly('solana', req)) 186 | .to.be.true; 187 | expect(response).to.deep.equal(res); 188 | }); 189 | }); 190 | 191 | describe('stake', () => { 192 | it('should stake with the correct parameters', async () => { 193 | const network = 'mainnet'; 194 | const walletAddress = 'some-wallet-address'; 195 | const amount = '100'; 196 | const validatorAddress = 'some-validator-address'; 197 | const req: CreateWorkflowRequest = { 198 | workflow: { 199 | action: `protocols/solana/networks/${network}/actions/stake`, 200 | solanaStakingParameters: { 201 | stakeParameters: { 202 | walletAddress: walletAddress, 203 | validatorAddress: validatorAddress, 204 | amount: { 205 | value: amount, 206 | currency: 'SOL', 207 | }, 208 | }, 209 | }, 210 | }, 211 | }; 212 | const res: Workflow = { 213 | /* response data */ 214 | }; 215 | 216 | stakingClientStub.createWorkflow.resolves(res); 217 | 218 | const response = await solana.stake( 219 | network, 220 | walletAddress, 221 | amount, 222 | validatorAddress, 223 | ); 224 | 225 | expect(stakingClientStub.createWorkflow.calledOnceWithExactly(req)).to.be 226 | .true; 227 | expect(response).to.deep.equal(res); 228 | }); 229 | 230 | it('should handle missing validator address', async () => { 231 | const network = 'mainnet'; 232 | const walletAddress = 'some-wallet-address'; 233 | const amount = '100'; 234 | const req: CreateWorkflowRequest = { 235 | workflow: { 236 | action: `protocols/solana/networks/${network}/actions/stake`, 237 | solanaStakingParameters: { 238 | stakeParameters: { 239 | walletAddress: walletAddress, 240 | validatorAddress: undefined, 241 | amount: { 242 | value: amount, 243 | currency: 'SOL', 244 | }, 245 | }, 246 | }, 247 | }, 248 | }; 249 | const res: Workflow = { 250 | /* response data */ 251 | }; 252 | 253 | stakingClientStub.createWorkflow.resolves(res); 254 | 255 | const response = await solana.stake(network, walletAddress, amount); 256 | 257 | expect(stakingClientStub.createWorkflow.calledOnceWithExactly(req)).to.be 258 | .true; 259 | expect(response).to.deep.equal(res); 260 | }); 261 | }); 262 | 263 | describe('unstake', () => { 264 | it('should unstake with the correct parameters', async () => { 265 | const network = 'mainnet'; 266 | const walletAddress = 'some-wallet-address'; 267 | const stakeAccountAddress = 'some-stake-account-address'; 268 | const amount = '100'; 269 | const req: CreateWorkflowRequest = { 270 | workflow: { 271 | action: `protocols/solana/networks/${network}/actions/unstake`, 272 | solanaStakingParameters: { 273 | unstakeParameters: { 274 | walletAddress: walletAddress, 275 | stakeAccountAddress: stakeAccountAddress, 276 | amount: { 277 | value: amount, 278 | currency: 'SOL', 279 | }, 280 | }, 281 | }, 282 | }, 283 | }; 284 | const res: Workflow = { 285 | /* response data */ 286 | }; 287 | 288 | stakingClientStub.createWorkflow.resolves(res); 289 | 290 | const response = await solana.unstake( 291 | network, 292 | walletAddress, 293 | stakeAccountAddress, 294 | amount, 295 | ); 296 | 297 | expect(stakingClientStub.createWorkflow.calledOnceWithExactly(req)).to.be 298 | .true; 299 | expect(response).to.deep.equal(res); 300 | }); 301 | }); 302 | }); 303 | -------------------------------------------------------------------------------- /src/client/protocols/solana-staking.ts: -------------------------------------------------------------------------------- 1 | import { StakingClient } from '../staking-client'; 2 | import { 3 | CreateWorkflowRequest, 4 | Workflow, 5 | } from '../../gen/coinbase/staking/orchestration/v1/workflow.pb'; 6 | import { 7 | ViewStakingContextRequest, 8 | ViewStakingContextResponse, 9 | } from '../../gen/coinbase/staking/orchestration/v1/staking_context.pb'; 10 | import { 11 | ListRewardsRequest, 12 | ListRewardsResponse, 13 | } from '../../gen/coinbase/staking/rewards/v1/reward.pb'; 14 | import { 15 | ListStakesRequest, 16 | ListStakesResponse, 17 | } from '../../gen/coinbase/staking/rewards/v1/stake.pb'; 18 | 19 | export class Solana { 20 | private parent: StakingClient; 21 | 22 | constructor(parent: StakingClient) { 23 | this.parent = parent; 24 | } 25 | 26 | async stake( 27 | network: string, 28 | walletAddress: string, 29 | amount: string, 30 | validatorAddress?: string, 31 | ): Promise { 32 | const req: CreateWorkflowRequest = { 33 | workflow: { 34 | action: `protocols/solana/networks/${network}/actions/stake`, 35 | solanaStakingParameters: { 36 | stakeParameters: { 37 | walletAddress: walletAddress, 38 | validatorAddress: validatorAddress, 39 | amount: { 40 | value: amount, 41 | currency: 'SOL', 42 | }, 43 | }, 44 | }, 45 | }, 46 | }; 47 | 48 | return this.parent.createWorkflow(req); 49 | } 50 | 51 | async unstake( 52 | network: string, 53 | walletAddress: string, 54 | stakeAccountAddress: string, 55 | amount: string, 56 | ): Promise { 57 | const req: CreateWorkflowRequest = { 58 | workflow: { 59 | action: `protocols/solana/networks/${network}/actions/unstake`, 60 | solanaStakingParameters: { 61 | unstakeParameters: { 62 | walletAddress: walletAddress, 63 | stakeAccountAddress: stakeAccountAddress, 64 | amount: { 65 | value: amount, 66 | currency: 'SOL', 67 | }, 68 | }, 69 | }, 70 | }, 71 | }; 72 | 73 | return this.parent.createWorkflow(req); 74 | } 75 | 76 | async claimStake( 77 | network: string, 78 | walletAddress: string, 79 | stakeAccountAddress: string, 80 | ): Promise { 81 | const req: CreateWorkflowRequest = { 82 | workflow: { 83 | action: `protocols/solana/networks/${network}/actions/claim_stake`, 84 | solanaStakingParameters: { 85 | claimStakeParameters: { 86 | walletAddress: walletAddress, 87 | stakeAccountAddress: stakeAccountAddress, 88 | }, 89 | }, 90 | }, 91 | }; 92 | 93 | return this.parent.createWorkflow(req); 94 | } 95 | 96 | async viewStakingContext( 97 | address: string, 98 | network: string, 99 | ): Promise { 100 | const req: ViewStakingContextRequest = { 101 | address: address, 102 | network: `protocols/solana/networks/${network}`, 103 | }; 104 | 105 | return this.parent.viewStakingContext(req); 106 | } 107 | 108 | async listRewards( 109 | filter: string, 110 | pageSize: number = 100, 111 | pageToken?: string, 112 | ): Promise { 113 | const req: ListRewardsRequest = { 114 | parent: 'protocols/solana', 115 | filter: filter, 116 | pageSize: pageSize, 117 | pageToken: pageToken, 118 | }; 119 | 120 | return this.parent.listRewards('solana', req); 121 | } 122 | 123 | async listStakes( 124 | filter: string, 125 | pageSize: number = 100, 126 | pageToken?: string, 127 | ): Promise { 128 | const req: ListStakesRequest = { 129 | parent: 'protocols/solana', 130 | filter: filter, 131 | pageSize: pageSize, 132 | pageToken: pageToken, 133 | }; 134 | 135 | return this.parent.listStakes('solana', req); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/client/staking-client.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import proxyquire from 'proxyquire'; 4 | import { StakingService } from '../gen/coinbase/staking/orchestration/v1/api.pb'; 5 | import { RewardService } from '../gen/coinbase/staking/rewards/v1/reward_service.pb'; 6 | import { 7 | CreateWorkflowRequest, 8 | Workflow, 9 | GetWorkflowRequest, 10 | ListWorkflowsRequest, 11 | ListWorkflowsResponse, 12 | } from '../gen/coinbase/staking/orchestration/v1/workflow.pb'; 13 | import { 14 | ViewStakingContextRequest, 15 | ViewStakingContextResponse, 16 | } from '../gen/coinbase/staking/orchestration/v1/staking_context.pb'; 17 | import { 18 | ListNetworksRequest, 19 | ListNetworksResponse, 20 | } from '../gen/coinbase/staking/orchestration/v1/network.pb'; 21 | import { 22 | ListActionsRequest, 23 | ListActionsResponse, 24 | } from '../gen/coinbase/staking/orchestration/v1/action.pb'; 25 | import { 26 | ListProtocolsRequest, 27 | ListProtocolsResponse, 28 | } from '../gen/coinbase/staking/orchestration/v1/protocol.pb'; 29 | import { 30 | ListRewardsRequest, 31 | ListRewardsResponse, 32 | } from '../gen/coinbase/staking/rewards/v1/reward.pb'; 33 | import { 34 | ListStakesRequest, 35 | ListStakesResponse, 36 | } from '../gen/coinbase/staking/rewards/v1/stake.pb'; 37 | import { StakingClient as StakingClientType } from './staking-client'; 38 | 39 | describe('StakingClient', () => { 40 | let buildJWTStub: sinon.SinonStub; 41 | let listProtocolsStub: sinon.SinonStub; 42 | let listNetworksStub: sinon.SinonStub; 43 | let listActionsStub: sinon.SinonStub; 44 | let viewStakingContextStub: sinon.SinonStub; 45 | let createWorkflowStub: sinon.SinonStub; 46 | let getWorkflowStub: sinon.SinonStub; 47 | let listWorkflowsStub: sinon.SinonStub; 48 | let listRewardsStub: sinon.SinonStub; 49 | let listStakesStub: sinon.SinonStub; 50 | 51 | let StakingClient: typeof StakingClientType; 52 | 53 | beforeEach(() => { 54 | buildJWTStub = sinon.stub(); 55 | listProtocolsStub = sinon.stub(StakingService, 'ListProtocols'); 56 | listNetworksStub = sinon.stub(StakingService, 'ListNetworks'); 57 | listActionsStub = sinon.stub(StakingService, 'ListActions'); 58 | viewStakingContextStub = sinon.stub(StakingService, 'ViewStakingContext'); 59 | createWorkflowStub = sinon.stub(StakingService, 'CreateWorkflow'); 60 | getWorkflowStub = sinon.stub(StakingService, 'GetWorkflow'); 61 | listWorkflowsStub = sinon.stub(StakingService, 'ListWorkflows'); 62 | listRewardsStub = sinon.stub(RewardService, 'ListRewards'); 63 | listStakesStub = sinon.stub(RewardService, 'ListStakes'); 64 | 65 | StakingClient = proxyquire('./staking-client', { 66 | '../auth/jwt': { buildJWT: buildJWTStub }, 67 | }).StakingClient; 68 | }); 69 | 70 | afterEach(() => { 71 | sinon.restore(); 72 | }); 73 | 74 | describe('listProtocols', () => { 75 | it('should list protocols with the correct parameters', async () => { 76 | const client = new StakingClient( 77 | 'apiKeyName', 78 | '-----BEGIN EC PRIVATE KEY-----test-private-key-----END EC PRIVATE KEY-----', 79 | 'https://api.example.com', 80 | ); 81 | const req: ListProtocolsRequest = {}; 82 | const res: ListProtocolsResponse = { 83 | /* response data */ 84 | }; 85 | 86 | let stubToken = 'foobar'; 87 | 88 | buildJWTStub.resolves(stubToken); 89 | listProtocolsStub.resolves(res); 90 | 91 | const response = await client.listProtocols(); 92 | 93 | expect(buildJWTStub.calledOnce).to.be.true; 94 | expect( 95 | listProtocolsStub.calledOnceWithExactly(req, { 96 | pathPrefix: 'https://api.example.com/orchestration', 97 | headers: { 98 | Authorization: `Bearer ${stubToken}`, 99 | }, 100 | }), 101 | ).to.be.true; 102 | expect(response).to.deep.equal(res); 103 | }); 104 | }); 105 | 106 | describe('listNetworks', () => { 107 | it('should list networks with the correct parameters', async () => { 108 | const client = new StakingClient( 109 | 'apiKeyName', 110 | '-----BEGIN EC PRIVATE KEY-----test-private-key-----END EC PRIVATE KEY-----', 111 | 'https://api.example.com', 112 | ); 113 | 114 | let protocol = 'some-protocol'; 115 | const req: ListNetworksRequest = { 116 | parent: `protocols/${protocol}`, 117 | }; 118 | const res: ListNetworksResponse = { 119 | /* response data */ 120 | }; 121 | 122 | let stubToken = 'foobar'; 123 | 124 | buildJWTStub.resolves(stubToken); 125 | listNetworksStub.resolves(res); 126 | 127 | const response = await client.listNetworks(protocol); 128 | 129 | expect(buildJWTStub.calledOnce).to.be.true; 130 | expect( 131 | listNetworksStub.calledOnceWithExactly(req, { 132 | pathPrefix: 'https://api.example.com/orchestration', 133 | headers: { 134 | Authorization: `Bearer ${stubToken}`, 135 | }, 136 | }), 137 | ).to.be.true; 138 | expect(response).to.deep.equal(res); 139 | }); 140 | }); 141 | 142 | describe('listActions', () => { 143 | it('should list actions with the correct parameters', async () => { 144 | const client = new StakingClient( 145 | 'apiKeyName', 146 | '-----BEGIN EC PRIVATE KEY-----test-private-key-----END EC PRIVATE KEY-----', 147 | 'https://api.example.com', 148 | ); 149 | 150 | let protocol = 'some-protocol'; 151 | let network = 'some-network'; 152 | const req: ListActionsRequest = { 153 | parent: `protocols/${protocol}/networks/${network}`, 154 | }; 155 | const res: ListActionsResponse = { 156 | /* response data */ 157 | }; 158 | 159 | let stubToken = 'foobar'; 160 | 161 | buildJWTStub.resolves(stubToken); 162 | listActionsStub.resolves(res); 163 | 164 | const response = await client.listActions(protocol, network); 165 | 166 | expect(buildJWTStub.calledOnce).to.be.true; 167 | expect( 168 | listActionsStub.calledOnceWithExactly(req, { 169 | pathPrefix: 'https://api.example.com/orchestration', 170 | headers: { 171 | Authorization: `Bearer ${stubToken}`, 172 | }, 173 | }), 174 | ).to.be.true; 175 | expect(response).to.deep.equal(res); 176 | }); 177 | }); 178 | 179 | describe('viewStakingContext', () => { 180 | it('should view staking context with the correct parameters', async () => { 181 | const client = new StakingClient( 182 | 'apiKeyName', 183 | '-----BEGIN EC PRIVATE KEY-----test-private-key-----END EC PRIVATE KEY-----', 184 | 'https://api.example.com', 185 | ); 186 | const req: ViewStakingContextRequest = { 187 | address: 'some-address', 188 | network: 'mainnet', 189 | }; 190 | const res: ViewStakingContextResponse = { 191 | /* response data */ 192 | }; 193 | 194 | let stubToken = 'foobar'; 195 | 196 | buildJWTStub.resolves(stubToken); 197 | viewStakingContextStub.resolves(res); 198 | 199 | const response = await client.viewStakingContext(req); 200 | 201 | expect(buildJWTStub.calledOnce).to.be.true; 202 | expect( 203 | viewStakingContextStub.calledOnceWithExactly(req, { 204 | pathPrefix: 'https://api.example.com/orchestration', 205 | headers: { 206 | Authorization: `Bearer ${stubToken}`, 207 | }, 208 | }), 209 | ).to.be.true; 210 | expect(response).to.deep.equal(res); 211 | }); 212 | }); 213 | 214 | describe('createWorkflow', () => { 215 | it('should create a workflow with the correct parameters', async () => { 216 | const client = new StakingClient( 217 | 'apiKeyName', 218 | '-----BEGIN EC PRIVATE KEY-----\ntest-private-key\n-----END EC PRIVATE KEY-----', 219 | 'https://api.example.com', 220 | ); 221 | const req: CreateWorkflowRequest = { 222 | /* request data */ 223 | }; 224 | const res: Workflow = { 225 | /* response data */ 226 | }; 227 | 228 | let stubToken = 'foobar'; 229 | 230 | buildJWTStub.resolves(stubToken); 231 | createWorkflowStub.resolves(res); 232 | 233 | const response = await client.createWorkflow(req); 234 | 235 | expect(buildJWTStub.calledOnce).to.be.true; 236 | expect( 237 | createWorkflowStub.calledOnceWithExactly(req, { 238 | pathPrefix: 'https://api.example.com/orchestration', 239 | headers: { 240 | Authorization: `Bearer ${stubToken}`, 241 | }, 242 | }), 243 | ).to.be.true; 244 | expect(response).to.deep.equal(res); 245 | }); 246 | }); 247 | 248 | describe('getWorkflow', () => { 249 | it('should get a workflow with the correct parameters', async () => { 250 | const client = new StakingClient( 251 | 'apiKeyName', 252 | '-----BEGIN EC PRIVATE KEY-----\ntest-private-key\n-----END EC PRIVATE KEY-----', 253 | 'https://api.example.com', 254 | ); 255 | 256 | const workflowName = 'workflow-id'; 257 | 258 | const getWorkflowRequest: GetWorkflowRequest = { 259 | name: workflowName, 260 | }; 261 | 262 | const res: Workflow = { 263 | /* response data */ 264 | }; 265 | 266 | let stubToken = 'foobar'; 267 | 268 | buildJWTStub.resolves(stubToken); 269 | getWorkflowStub.resolves(res); 270 | 271 | const response = await client.getWorkflow(workflowName); 272 | 273 | expect(buildJWTStub.calledOnce).to.be.true; 274 | expect( 275 | getWorkflowStub.calledOnceWithExactly(getWorkflowRequest, { 276 | pathPrefix: 'https://api.example.com/orchestration', 277 | headers: { 278 | Authorization: `Bearer ${stubToken}`, 279 | }, 280 | }), 281 | ).to.be.true; 282 | expect(response).to.deep.equal(res); 283 | }); 284 | }); 285 | 286 | describe('listWorkflows', () => { 287 | it('should list workflows with the correct parameters', async () => { 288 | const client = new StakingClient( 289 | 'apiKeyName', 290 | '-----BEGIN EC PRIVATE KEY-----test-private-key-----END EC PRIVATE KEY-----', 291 | 'https://api.example.com', 292 | ); 293 | const req: ListWorkflowsRequest = { 294 | pageSize: 100, 295 | filter: 'foobar', 296 | }; 297 | const res: ListWorkflowsResponse = { 298 | /* response data */ 299 | }; 300 | 301 | let stubToken = 'foobar'; 302 | 303 | buildJWTStub.resolves(stubToken); 304 | listWorkflowsStub.resolves(res); 305 | 306 | const response = await client.listWorkflows(100, 'foobar'); 307 | 308 | expect(buildJWTStub.calledOnce).to.be.true; 309 | expect( 310 | listWorkflowsStub.calledOnceWithExactly(req, { 311 | pathPrefix: 'https://api.example.com/orchestration', 312 | headers: { 313 | Authorization: `Bearer ${stubToken}`, 314 | }, 315 | }), 316 | ).to.be.true; 317 | expect(response).to.deep.equal(res); 318 | }); 319 | }); 320 | 321 | describe('listRewards', () => { 322 | it('should list rewards with the correct parameters', async () => { 323 | const client = new StakingClient( 324 | 'apiKeyName', 325 | '-----BEGIN EC PRIVATE KEY-----test-private-key-----END EC PRIVATE KEY-----', 326 | 'https://api.example.com', 327 | ); 328 | let protocol = 'ethereum'; 329 | const req: ListRewardsRequest = { 330 | pageSize: 100, 331 | filter: 'foobar', 332 | }; 333 | const res: ListRewardsResponse = { 334 | /* response data */ 335 | }; 336 | 337 | let stubToken = 'foobar'; 338 | 339 | buildJWTStub.resolves(stubToken); 340 | listRewardsStub.resolves(res); 341 | 342 | const response = await client.listRewards(protocol, req); 343 | 344 | expect(buildJWTStub.calledOnce).to.be.true; 345 | expect( 346 | listRewardsStub.calledOnceWithExactly(req, { 347 | pathPrefix: 'https://api.example.com/rewards', 348 | headers: { 349 | Authorization: `Bearer ${stubToken}`, 350 | }, 351 | }), 352 | ).to.be.true; 353 | expect(response).to.deep.equal(res); 354 | }); 355 | }); 356 | 357 | describe('listStakes', () => { 358 | it('should list stakes with the correct parameters', async () => { 359 | const client = new StakingClient( 360 | 'apiKeyName', 361 | '-----BEGIN EC PRIVATE KEY-----test-private-key-----END EC PRIVATE KEY-----', 362 | 'https://api.example.com', 363 | ); 364 | let protocol = 'ethereum'; 365 | const req: ListStakesRequest = { 366 | pageSize: 100, 367 | filter: 'foobar', 368 | }; 369 | const res: ListStakesResponse = { 370 | /* response data */ 371 | }; 372 | 373 | let stubToken = 'foobar'; 374 | 375 | buildJWTStub.resolves(stubToken); 376 | listStakesStub.resolves(res); 377 | 378 | const response = await client.listStakes(protocol, req); 379 | 380 | expect(buildJWTStub.calledOnce).to.be.true; 381 | expect( 382 | listStakesStub.calledOnceWithExactly(req, { 383 | pathPrefix: 'https://api.example.com/rewards', 384 | headers: { 385 | Authorization: `Bearer ${stubToken}`, 386 | }, 387 | }), 388 | ).to.be.true; 389 | expect(response).to.deep.equal(res); 390 | }); 391 | }); 392 | }); 393 | -------------------------------------------------------------------------------- /src/client/staking-client.ts: -------------------------------------------------------------------------------- 1 | import { StakingService } from '../gen/coinbase/staking/orchestration/v1/api.pb'; 2 | import { RewardService } from '../gen/coinbase/staking/rewards/v1/reward_service.pb'; 3 | import { 4 | ListProtocolsRequest, 5 | ListProtocolsResponse, 6 | } from '../gen/coinbase/staking/orchestration/v1/protocol.pb'; 7 | import { 8 | ListNetworksRequest, 9 | ListNetworksResponse, 10 | } from '../gen/coinbase/staking/orchestration/v1/network.pb'; 11 | import { 12 | ListActionsRequest, 13 | ListActionsResponse, 14 | } from '../gen/coinbase/staking/orchestration/v1/action.pb'; 15 | import * as fm from '../gen/fetch.pb'; 16 | import { buildJWT } from '../auth/jwt'; 17 | import { 18 | ViewStakingContextRequest, 19 | ViewStakingContextResponse, 20 | } from '../gen/coinbase/staking/orchestration/v1/staking_context.pb'; 21 | import { 22 | CreateWorkflowRequest, 23 | GetWorkflowRequest, 24 | ListWorkflowsRequest, 25 | ListWorkflowsResponse, 26 | Workflow, 27 | WorkflowState, 28 | WorkflowStep, 29 | TxStepOutput, 30 | WaitStepOutput, 31 | } from '../gen/coinbase/staking/orchestration/v1/workflow.pb'; 32 | import { 33 | ListRewardsRequest, 34 | ListRewardsResponse, 35 | } from '../gen/coinbase/staking/rewards/v1/reward.pb'; 36 | import { 37 | ListStakesRequest, 38 | ListStakesResponse, 39 | } from '../gen/coinbase/staking/rewards/v1/stake.pb'; 40 | 41 | import { Ethereum } from './protocols/ethereum-kiln-staking'; 42 | import { Solana } from './protocols/solana-staking'; 43 | 44 | const DEFAULT_URL = 'https://api.developer.coinbase.com/staking'; 45 | 46 | export class StakingClient { 47 | readonly baseURL: string; 48 | readonly apiKeyName: string | undefined; 49 | readonly apiPrivateKey: string | undefined; 50 | readonly Ethereum: Ethereum; 51 | readonly Solana: Solana; 52 | 53 | constructor(apiKeyName?: string, apiPrivateKey?: string, baseURL?: string) { 54 | if (baseURL) { 55 | this.baseURL = baseURL; 56 | } else { 57 | this.baseURL = DEFAULT_URL; 58 | } 59 | 60 | if (apiKeyName) { 61 | this.apiKeyName = apiKeyName; 62 | } 63 | 64 | if (apiPrivateKey) { 65 | this.apiPrivateKey = apiPrivateKey; 66 | } 67 | 68 | this.Ethereum = new Ethereum(this); 69 | this.Solana = new Solana(this); 70 | } 71 | 72 | // List protocols supported by Staking API 73 | async listProtocols(): Promise { 74 | const path: string = '/v1/protocols'; 75 | const method: string = 'GET'; 76 | const url: string = this.baseURL + '/orchestration'; 77 | 78 | // Generate the JWT token and get the auth details as a initReq object. 79 | const initReq = await getAuthDetails( 80 | url, 81 | path, 82 | method, 83 | this.apiKeyName, 84 | this.apiPrivateKey, 85 | ); 86 | 87 | const req: ListProtocolsRequest = {}; 88 | 89 | return StakingService.ListProtocols(req, initReq); 90 | } 91 | 92 | // List networks supported by Staking API for a given protocol. 93 | async listNetworks(protocol: string): Promise { 94 | const parent: string = `protocols/${protocol}`; 95 | const path: string = `/v1/${parent}/networks`; 96 | const method: string = 'GET'; 97 | const url: string = this.baseURL + '/orchestration'; 98 | 99 | // Generate the JWT token and get the auth details as a initReq object. 100 | const initReq = await getAuthDetails( 101 | url, 102 | path, 103 | method, 104 | this.apiKeyName, 105 | this.apiPrivateKey, 106 | ); 107 | 108 | const req: ListNetworksRequest = { 109 | parent: parent, 110 | }; 111 | 112 | return StakingService.ListNetworks(req, initReq); 113 | } 114 | 115 | // List actions supported by Staking API for a given protocol and network. 116 | async listActions( 117 | protocol: string, 118 | network: string, 119 | ): Promise { 120 | const parent: string = `protocols/${protocol}/networks/${network}`; 121 | const path: string = `/v1/${parent}/actions`; 122 | const method: string = 'GET'; 123 | const url: string = this.baseURL + '/orchestration'; 124 | 125 | // Generate the JWT token and get the auth details as a initReq object. 126 | const initReq = await getAuthDetails( 127 | url, 128 | path, 129 | method, 130 | this.apiKeyName, 131 | this.apiPrivateKey, 132 | ); 133 | const req: ListActionsRequest = { 134 | parent: parent, 135 | }; 136 | 137 | return StakingService.ListActions(req, initReq); 138 | } 139 | 140 | // Returns point-in-time context of staking data for an address. This function takes the entire req object as input. 141 | // Use the protocol-specific helper functions like Ethereum.ViewStakingContext to view protocol and network 142 | // specific staking context. 143 | async viewStakingContext( 144 | req: ViewStakingContextRequest, 145 | ): Promise { 146 | const path: string = '/v1/viewStakingContext:view'; 147 | const method: string = 'GET'; 148 | const url: string = this.baseURL + '/orchestration'; 149 | 150 | // Generate the JWT token and get the auth details as a initReq object. 151 | const initReq = await getAuthDetails( 152 | url, 153 | path, 154 | method, 155 | this.apiKeyName, 156 | this.apiPrivateKey, 157 | ); 158 | 159 | return StakingService.ViewStakingContext(req, initReq); 160 | } 161 | 162 | // Create a workflow. This function takes the entire req object as input. 163 | // Use the protocol-specific helper functions like Ethereum.Stake to create a protocol and action specific workflow. 164 | async createWorkflow(req: CreateWorkflowRequest): Promise { 165 | const path: string = `/v1/workflows`; 166 | const method: string = 'POST'; 167 | const url: string = this.baseURL + '/orchestration'; 168 | 169 | // Generate the JWT token and get the auth details as a initReq object. 170 | const initReq = await getAuthDetails( 171 | url, 172 | path, 173 | method, 174 | this.apiKeyName, 175 | this.apiPrivateKey, 176 | ); 177 | 178 | return StakingService.CreateWorkflow(req, initReq); 179 | } 180 | 181 | // Get a workflow given workflow id. 182 | async getWorkflow(workflowName: string): Promise { 183 | const path: string = `/v1/${workflowName}`; 184 | const method: string = 'GET'; 185 | const url: string = this.baseURL + '/orchestration'; 186 | 187 | // Generate the JWT token and get the auth details as a initReq object. 188 | const initReq = await getAuthDetails( 189 | url, 190 | path, 191 | method, 192 | this.apiKeyName, 193 | this.apiPrivateKey, 194 | ); 195 | 196 | const req: GetWorkflowRequest = { 197 | name: workflowName, 198 | }; 199 | 200 | return StakingService.GetWorkflow(req, initReq); 201 | } 202 | 203 | // List your workflows. 204 | async listWorkflows( 205 | pageSize: number = 100, 206 | filter: string = '', 207 | ): Promise { 208 | const path: string = `/v1/workflows`; 209 | const method: string = 'GET'; 210 | const url: string = this.baseURL + '/orchestration'; 211 | 212 | // Generate the JWT token and get the auth details as a initReq object. 213 | const initReq = await getAuthDetails( 214 | url, 215 | path, 216 | method, 217 | this.apiKeyName, 218 | this.apiPrivateKey, 219 | ); 220 | 221 | const req: ListWorkflowsRequest = { 222 | pageSize: pageSize, 223 | filter: filter, 224 | }; 225 | 226 | return StakingService.ListWorkflows(req, initReq); 227 | } 228 | 229 | // List onchain rewards of an address for a specific protocol, with optional filters for time range, aggregation period, and more. 230 | async listRewards( 231 | protocol: string, 232 | req: ListRewardsRequest, 233 | ): Promise { 234 | const parent: string = `protocols/${protocol}`; 235 | const path: string = `/v1/${parent}/rewards`; 236 | const method: string = 'GET'; 237 | const url: string = this.baseURL + '/rewards'; 238 | 239 | // Generate the JWT token and get the auth details as a initReq object. 240 | const initReq = await getAuthDetails( 241 | url, 242 | path, 243 | method, 244 | this.apiKeyName, 245 | this.apiPrivateKey, 246 | ); 247 | 248 | return RewardService.ListRewards(req, initReq); 249 | } 250 | 251 | // List staking activities for a given protocol. 252 | async listStakes( 253 | protocol: string, 254 | req: ListStakesRequest, 255 | ): Promise { 256 | const parent: string = `protocols/${protocol}`; 257 | const path: string = `/v1/${parent}/stakes`; 258 | const method: string = 'GET'; 259 | const url: string = this.baseURL + '/rewards'; 260 | 261 | // Generate the JWT token and get the auth details as a initReq object. 262 | const initReq = await getAuthDetails( 263 | url, 264 | path, 265 | method, 266 | this.apiKeyName, 267 | this.apiPrivateKey, 268 | ); 269 | 270 | return RewardService.ListStakes(req, initReq); 271 | } 272 | } 273 | 274 | export function workflowHasFinished(workflow: Workflow): boolean { 275 | return ( 276 | workflow.state === WorkflowState.STATE_COMPLETED || 277 | workflow.state === WorkflowState.STATE_FAILED 278 | ); 279 | } 280 | 281 | export function workflowWaitingForExternalBroadcast( 282 | workflow: Workflow, 283 | ): boolean { 284 | return workflow.state === WorkflowState.STATE_WAITING_FOR_EXT_BROADCAST; 285 | } 286 | 287 | export function isTxStepOutput( 288 | step: WorkflowStep, 289 | ): step is WorkflowStep & { txStepOutput: TxStepOutput } { 290 | return (step as WorkflowStep).txStepOutput !== undefined; 291 | } 292 | 293 | export function isWaitStepOutput( 294 | step: WorkflowStep, 295 | ): step is WorkflowStep & { waitStepOutput: WaitStepOutput } { 296 | return (step as WorkflowStep).waitStepOutput !== undefined; 297 | } 298 | 299 | export function getUnsignedTx(workflow: Workflow): string { 300 | if (workflow.steps === undefined) { 301 | throw new Error('No steps found in workflow'); 302 | } 303 | 304 | if (workflow.currentStepId === undefined) { 305 | throw new Error('No current step found in workflow'); 306 | } 307 | 308 | if (workflow.steps[workflow.currentStepId].txStepOutput === undefined) { 309 | throw new Error( 310 | `No txStepOutput found in workflow step ${workflow.currentStepId}`, 311 | ); 312 | } 313 | 314 | return ( 315 | workflow.steps[workflow.currentStepId].txStepOutput?.unsignedTx 316 | ); 317 | } 318 | 319 | export async function getAuthDetails( 320 | url: string, 321 | path: string, 322 | method: string, 323 | apiKeyName?: string, 324 | apiPrivateKey?: string, 325 | ): Promise { 326 | // Generate the JWT token 327 | const token = await buildJWT(url + path, method, apiKeyName, apiPrivateKey); 328 | 329 | return { 330 | pathPrefix: url, 331 | headers: { 332 | Authorization: `Bearer ${token}`, 333 | }, 334 | }; 335 | } 336 | -------------------------------------------------------------------------------- /src/gen/coinbase/staking/orchestration/v1/action.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | export type Action = { 7 | name?: string 8 | } 9 | 10 | export type ListActionsRequest = { 11 | parent?: string 12 | } 13 | 14 | export type ListActionsResponse = { 15 | actions?: Action[] 16 | } -------------------------------------------------------------------------------- /src/gen/coinbase/staking/orchestration/v1/api.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | import * as fm from "../../../../fetch.pb" 8 | import * as CoinbaseStakingOrchestrationV1Action from "./action.pb" 9 | import * as CoinbaseStakingOrchestrationV1Network from "./network.pb" 10 | import * as CoinbaseStakingOrchestrationV1Protocol from "./protocol.pb" 11 | import * as CoinbaseStakingOrchestrationV1Staking_context from "./staking_context.pb" 12 | import * as CoinbaseStakingOrchestrationV1Staking_target from "./staking_target.pb" 13 | import * as CoinbaseStakingOrchestrationV1Workflow from "./workflow.pb" 14 | export class StakingService { 15 | static ListProtocols(req: CoinbaseStakingOrchestrationV1Protocol.ListProtocolsRequest, initReq?: fm.InitReq): Promise { 16 | return fm.fetchReq(`/v1/protocols?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"}) 17 | } 18 | static ListNetworks(req: CoinbaseStakingOrchestrationV1Network.ListNetworksRequest, initReq?: fm.InitReq): Promise { 19 | return fm.fetchReq(`/v1/${req["parent"]}/networks?${fm.renderURLSearchParams(req, ["parent"])}`, {...initReq, method: "GET"}) 20 | } 21 | static ListStakingTargets(req: CoinbaseStakingOrchestrationV1Staking_target.ListStakingTargetsRequest, initReq?: fm.InitReq): Promise { 22 | return fm.fetchReq(`/v1/${req["parent"]}/stakingTargets?${fm.renderURLSearchParams(req, ["parent"])}`, {...initReq, method: "GET"}) 23 | } 24 | static ListActions(req: CoinbaseStakingOrchestrationV1Action.ListActionsRequest, initReq?: fm.InitReq): Promise { 25 | return fm.fetchReq(`/v1/${req["parent"]}/actions?${fm.renderURLSearchParams(req, ["parent"])}`, {...initReq, method: "GET"}) 26 | } 27 | static CreateWorkflow(req: CoinbaseStakingOrchestrationV1Workflow.CreateWorkflowRequest, initReq?: fm.InitReq): Promise { 28 | return fm.fetchReq(`/v1/workflows`, {...initReq, method: "POST", body: JSON.stringify(req["workflow"], fm.replacer)}) 29 | } 30 | static GetWorkflow(req: CoinbaseStakingOrchestrationV1Workflow.GetWorkflowRequest, initReq?: fm.InitReq): Promise { 31 | return fm.fetchReq(`/v1/${req["name"]}?${fm.renderURLSearchParams(req, ["name"])}`, {...initReq, method: "GET"}) 32 | } 33 | static ListWorkflows(req: CoinbaseStakingOrchestrationV1Workflow.ListWorkflowsRequest, initReq?: fm.InitReq): Promise { 34 | return fm.fetchReq(`/v1/workflows?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"}) 35 | } 36 | static PerformWorkflowStep(req: CoinbaseStakingOrchestrationV1Workflow.PerformWorkflowStepRequest, initReq?: fm.InitReq): Promise { 37 | return fm.fetchReq(`/v1/${req["name"]}/step`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)}) 38 | } 39 | static ViewStakingContext(req: CoinbaseStakingOrchestrationV1Staking_context.ViewStakingContextRequest, initReq?: fm.InitReq): Promise { 40 | return fm.fetchReq(`/v1/viewStakingContext:view?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"}) 41 | } 42 | } -------------------------------------------------------------------------------- /src/gen/coinbase/staking/orchestration/v1/common.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | export type Amount = { 7 | value?: string 8 | currency?: string 9 | } -------------------------------------------------------------------------------- /src/gen/coinbase/staking/orchestration/v1/ethereum.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | import * as CoinbaseStakingOrchestrationV1Common from "./common.pb" 8 | 9 | type Absent = { [k in Exclude]?: undefined }; 10 | type OneOf = 11 | | { [k in keyof T]?: undefined } 12 | | ( 13 | keyof T extends infer K ? 14 | (K extends string & keyof T ? { [k in K]: T[K] } & Absent 15 | : never) 16 | : never); 17 | 18 | type BaseEthereumStakingParameters = { 19 | } 20 | 21 | export type EthereumStakingParameters = BaseEthereumStakingParameters 22 | & OneOf<{ nativeStakeParameters: EthereumNativeStakeParameters }> 23 | 24 | export type EthereumNativeStakeParameters = { 25 | withdrawalAddress?: string 26 | feeRecipient?: string 27 | amount?: CoinbaseStakingOrchestrationV1Common.Amount 28 | } -------------------------------------------------------------------------------- /src/gen/coinbase/staking/orchestration/v1/ethereum_kiln.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | import * as CoinbaseStakingOrchestrationV1Common from "./common.pb" 8 | 9 | type Absent = { [k in Exclude]?: undefined }; 10 | type OneOf = 11 | | { [k in keyof T]?: undefined } 12 | | ( 13 | keyof T extends infer K ? 14 | (K extends string & keyof T ? { [k in K]: T[K] } & Absent 15 | : never) 16 | : never); 17 | export type EthereumKilnStakeParameters = { 18 | stakerAddress?: string 19 | integratorContractAddress?: string 20 | amount?: CoinbaseStakingOrchestrationV1Common.Amount 21 | } 22 | 23 | export type EthereumKilnUnstakeParameters = { 24 | stakerAddress?: string 25 | integratorContractAddress?: string 26 | amount?: CoinbaseStakingOrchestrationV1Common.Amount 27 | } 28 | 29 | export type EthereumKilnClaimStakeParameters = { 30 | stakerAddress?: string 31 | integratorContractAddress?: string 32 | } 33 | 34 | 35 | type BaseEthereumKilnStakingParameters = { 36 | } 37 | 38 | export type EthereumKilnStakingParameters = BaseEthereumKilnStakingParameters 39 | & OneOf<{ stakeParameters: EthereumKilnStakeParameters; unstakeParameters: EthereumKilnUnstakeParameters; claimStakeParameters: EthereumKilnClaimStakeParameters }> 40 | 41 | export type EthereumKilnStakingContextParameters = { 42 | integratorContractAddress?: string 43 | } 44 | 45 | export type EthereumKilnStakingContextDetails = { 46 | ethereumBalance?: CoinbaseStakingOrchestrationV1Common.Amount 47 | integratorShareBalance?: CoinbaseStakingOrchestrationV1Common.Amount 48 | integratorShareUnderlyingBalance?: CoinbaseStakingOrchestrationV1Common.Amount 49 | totalExitableEth?: CoinbaseStakingOrchestrationV1Common.Amount 50 | totalSharesPendingExit?: CoinbaseStakingOrchestrationV1Common.Amount 51 | fulfillableShareCount?: CoinbaseStakingOrchestrationV1Common.Amount 52 | } -------------------------------------------------------------------------------- /src/gen/coinbase/staking/orchestration/v1/network.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | export type Network = { 7 | name?: string 8 | } 9 | 10 | export type ListNetworksRequest = { 11 | parent?: string 12 | } 13 | 14 | export type ListNetworksResponse = { 15 | networks?: Network[] 16 | } -------------------------------------------------------------------------------- /src/gen/coinbase/staking/orchestration/v1/protocol.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | export type Protocol = { 7 | name?: string 8 | } 9 | 10 | export type ListProtocolsRequest = { 11 | } 12 | 13 | export type ListProtocolsResponse = { 14 | protocols?: Protocol[] 15 | } -------------------------------------------------------------------------------- /src/gen/coinbase/staking/orchestration/v1/solana.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | import * as CoinbaseStakingOrchestrationV1Common from "./common.pb" 8 | 9 | type Absent = { [k in Exclude]?: undefined }; 10 | type OneOf = 11 | | { [k in keyof T]?: undefined } 12 | | ( 13 | keyof T extends infer K ? 14 | (K extends string & keyof T ? { [k in K]: T[K] } & Absent 15 | : never) 16 | : never); 17 | 18 | export enum StakeAccountBalanceState { 19 | BALANCE_STATE_UNSPECIFIED = "BALANCE_STATE_UNSPECIFIED", 20 | BALANCE_STATE_INACTIVE = "BALANCE_STATE_INACTIVE", 21 | BALANCE_STATE_ACTIVATING = "BALANCE_STATE_ACTIVATING", 22 | BALANCE_STATE_ACTIVE = "BALANCE_STATE_ACTIVE", 23 | BALANCE_STATE_DEACTIVATING = "BALANCE_STATE_DEACTIVATING", 24 | } 25 | 26 | export type PriorityFee = { 27 | computeUnitLimit?: string 28 | unitPrice?: string 29 | } 30 | 31 | export type SolanaStakeParameters = { 32 | walletAddress?: string 33 | validatorAddress?: string 34 | amount?: CoinbaseStakingOrchestrationV1Common.Amount 35 | priorityFee?: PriorityFee 36 | } 37 | 38 | export type SolanaUnstakeParameters = { 39 | walletAddress?: string 40 | stakeAccountAddress?: string 41 | amount?: CoinbaseStakingOrchestrationV1Common.Amount 42 | priorityFee?: PriorityFee 43 | } 44 | 45 | export type SolanaClaimStakeParameters = { 46 | walletAddress?: string 47 | stakeAccountAddress?: string 48 | priorityFee?: PriorityFee 49 | } 50 | 51 | export type SolanaStakingContextParameters = { 52 | } 53 | 54 | export type SolanaStakingContextDetails = { 55 | balance?: CoinbaseStakingOrchestrationV1Common.Amount 56 | currentEpoch?: string 57 | epochCompletionPercentage?: string 58 | stakeAccounts?: StakeAccount[] 59 | } 60 | 61 | export type StakeAccount = { 62 | address?: string 63 | bondedStake?: CoinbaseStakingOrchestrationV1Common.Amount 64 | rentReserve?: CoinbaseStakingOrchestrationV1Common.Amount 65 | balance?: CoinbaseStakingOrchestrationV1Common.Amount 66 | balanceState?: StakeAccountBalanceState 67 | validator?: string 68 | } 69 | 70 | 71 | type BaseSolanaStakingParameters = { 72 | } 73 | 74 | export type SolanaStakingParameters = BaseSolanaStakingParameters 75 | & OneOf<{ stakeParameters: SolanaStakeParameters; unstakeParameters: SolanaUnstakeParameters; claimStakeParameters: SolanaClaimStakeParameters }> -------------------------------------------------------------------------------- /src/gen/coinbase/staking/orchestration/v1/staking_context.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | import * as CoinbaseStakingOrchestrationV1Ethereum_kiln from "./ethereum_kiln.pb" 8 | import * as CoinbaseStakingOrchestrationV1Solana from "./solana.pb" 9 | 10 | type Absent = { [k in Exclude]?: undefined }; 11 | type OneOf = 12 | | { [k in keyof T]?: undefined } 13 | | ( 14 | keyof T extends infer K ? 15 | (K extends string & keyof T ? { [k in K]: T[K] } & Absent 16 | : never) 17 | : never); 18 | 19 | type BaseViewStakingContextRequest = { 20 | address?: string 21 | network?: string 22 | } 23 | 24 | export type ViewStakingContextRequest = BaseViewStakingContextRequest 25 | & OneOf<{ ethereumKilnStakingContextParameters: CoinbaseStakingOrchestrationV1Ethereum_kiln.EthereumKilnStakingContextParameters; solanaStakingContextParameters: CoinbaseStakingOrchestrationV1Solana.SolanaStakingContextParameters }> 26 | 27 | 28 | type BaseViewStakingContextResponse = { 29 | address?: string 30 | } 31 | 32 | export type ViewStakingContextResponse = BaseViewStakingContextResponse 33 | & OneOf<{ ethereumKilnStakingContextDetails: CoinbaseStakingOrchestrationV1Ethereum_kiln.EthereumKilnStakingContextDetails; solanaStakingContextDetails: CoinbaseStakingOrchestrationV1Solana.SolanaStakingContextDetails }> -------------------------------------------------------------------------------- /src/gen/coinbase/staking/orchestration/v1/staking_target.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | type Absent = { [k in Exclude]?: undefined }; 8 | type OneOf = 9 | | { [k in keyof T]?: undefined } 10 | | ( 11 | keyof T extends infer K ? 12 | (K extends string & keyof T ? { [k in K]: T[K] } & Absent 13 | : never) 14 | : never); 15 | export type Validator = { 16 | name?: string 17 | address?: string 18 | commissionRate?: number 19 | } 20 | 21 | export type Contract = { 22 | name?: string 23 | address?: string 24 | } 25 | 26 | 27 | type BaseStakingTarget = { 28 | } 29 | 30 | export type StakingTarget = BaseStakingTarget 31 | & OneOf<{ validator: Validator; contract: Contract }> 32 | 33 | export type ListStakingTargetsRequest = { 34 | parent?: string 35 | pageSize?: number 36 | pageToken?: string 37 | } 38 | 39 | export type ListStakingTargetsResponse = { 40 | stakingTargets?: StakingTarget[] 41 | nextPageToken?: string 42 | } -------------------------------------------------------------------------------- /src/gen/coinbase/staking/orchestration/v1/workflow.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | import * as GoogleProtobufTimestamp from "../../../../google/protobuf/timestamp.pb" 8 | import * as CoinbaseStakingOrchestrationV1Ethereum from "./ethereum.pb" 9 | import * as CoinbaseStakingOrchestrationV1Ethereum_kiln from "./ethereum_kiln.pb" 10 | import * as CoinbaseStakingOrchestrationV1Solana from "./solana.pb" 11 | 12 | type Absent = { [k in Exclude]?: undefined }; 13 | type OneOf = 14 | | { [k in keyof T]?: undefined } 15 | | ( 16 | keyof T extends infer K ? 17 | (K extends string & keyof T ? { [k in K]: T[K] } & Absent 18 | : never) 19 | : never); 20 | 21 | export enum TxStepOutputState { 22 | STATE_UNSPECIFIED = "STATE_UNSPECIFIED", 23 | STATE_NOT_CONSTRUCTED = "STATE_NOT_CONSTRUCTED", 24 | STATE_CONSTRUCTED = "STATE_CONSTRUCTED", 25 | STATE_PENDING_EXT_BROADCAST = "STATE_PENDING_EXT_BROADCAST", 26 | STATE_SIGNED = "STATE_SIGNED", 27 | STATE_BROADCASTING = "STATE_BROADCASTING", 28 | STATE_CONFIRMING = "STATE_CONFIRMING", 29 | STATE_CONFIRMED = "STATE_CONFIRMED", 30 | STATE_FINALIZED = "STATE_FINALIZED", 31 | STATE_FAILED = "STATE_FAILED", 32 | STATE_SUCCESS = "STATE_SUCCESS", 33 | } 34 | 35 | export enum WaitStepOutputWaitUnit { 36 | WAIT_UNIT_UNSPECIFIED = "WAIT_UNIT_UNSPECIFIED", 37 | WAIT_UNIT_SECONDS = "WAIT_UNIT_SECONDS", 38 | WAIT_UNIT_BLOCKS = "WAIT_UNIT_BLOCKS", 39 | WAIT_UNIT_EPOCHS = "WAIT_UNIT_EPOCHS", 40 | WAIT_UNIT_CHECKPOINTS = "WAIT_UNIT_CHECKPOINTS", 41 | } 42 | 43 | export enum WaitStepOutputState { 44 | STATE_UNSPECIFIED = "STATE_UNSPECIFIED", 45 | STATE_NOT_STARTED = "STATE_NOT_STARTED", 46 | STATE_IN_PROGRESS = "STATE_IN_PROGRESS", 47 | STATE_COMPLETED = "STATE_COMPLETED", 48 | } 49 | 50 | export enum ProvisionInfraStepOutputState { 51 | STATE_UNSPECIFIED = "STATE_UNSPECIFIED", 52 | STATE_IN_PROGRESS = "STATE_IN_PROGRESS", 53 | STATE_COMPLETED = "STATE_COMPLETED", 54 | STATE_FAILED = "STATE_FAILED", 55 | } 56 | 57 | export enum BulkTxStepOutputState { 58 | STATE_UNSPECIFIED = "STATE_UNSPECIFIED", 59 | STATE_IN_PROGRESS = "STATE_IN_PROGRESS", 60 | STATE_FAILED = "STATE_FAILED", 61 | STATE_COMPLETED = "STATE_COMPLETED", 62 | } 63 | 64 | export enum WorkflowState { 65 | STATE_UNSPECIFIED = "STATE_UNSPECIFIED", 66 | STATE_IN_PROGRESS = "STATE_IN_PROGRESS", 67 | STATE_WAITING_FOR_EXT_BROADCAST = "STATE_WAITING_FOR_EXT_BROADCAST", 68 | STATE_COMPLETED = "STATE_COMPLETED", 69 | STATE_FAILED = "STATE_FAILED", 70 | } 71 | 72 | export type TxStepOutput = { 73 | unsignedTx?: string 74 | signedTx?: string 75 | txHash?: string 76 | state?: TxStepOutputState 77 | errorMessage?: string 78 | } 79 | 80 | export type WaitStepOutput = { 81 | start?: string 82 | current?: string 83 | target?: string 84 | unit?: WaitStepOutputWaitUnit 85 | state?: WaitStepOutputState 86 | } 87 | 88 | export type ProvisionInfraStepOutput = { 89 | state?: ProvisionInfraStepOutputState 90 | } 91 | 92 | export type BulkTxStepOutput = { 93 | unsignedTxs?: string[] 94 | state?: BulkTxStepOutputState 95 | } 96 | 97 | 98 | type BaseWorkflowStep = { 99 | name?: string 100 | } 101 | 102 | export type WorkflowStep = BaseWorkflowStep 103 | & OneOf<{ txStepOutput: TxStepOutput; waitStepOutput: WaitStepOutput; provisionInfraStepOutput: ProvisionInfraStepOutput; bulkTxStepOutput: BulkTxStepOutput }> 104 | 105 | 106 | type BaseWorkflow = { 107 | name?: string 108 | action?: string 109 | state?: WorkflowState 110 | currentStepId?: number 111 | steps?: WorkflowStep[] 112 | createTime?: GoogleProtobufTimestamp.Timestamp 113 | updateTime?: GoogleProtobufTimestamp.Timestamp 114 | completeTime?: GoogleProtobufTimestamp.Timestamp 115 | } 116 | 117 | export type Workflow = BaseWorkflow 118 | & OneOf<{ solanaStakingParameters: CoinbaseStakingOrchestrationV1Solana.SolanaStakingParameters; ethereumKilnStakingParameters: CoinbaseStakingOrchestrationV1Ethereum_kiln.EthereumKilnStakingParameters; ethereumStakingParameters: CoinbaseStakingOrchestrationV1Ethereum.EthereumStakingParameters }> 119 | 120 | export type CreateWorkflowRequest = { 121 | workflow?: Workflow 122 | } 123 | 124 | export type GetWorkflowRequest = { 125 | name?: string 126 | } 127 | 128 | export type ListWorkflowsRequest = { 129 | filter?: string 130 | pageSize?: number 131 | pageToken?: string 132 | } 133 | 134 | export type ListWorkflowsResponse = { 135 | workflows?: Workflow[] 136 | nextPageToken?: string 137 | } 138 | 139 | export type PerformWorkflowStepRequest = { 140 | name?: string 141 | step?: number 142 | data?: string 143 | } -------------------------------------------------------------------------------- /src/gen/coinbase/staking/rewards/v1/common.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | export type AssetAmount = { 7 | amount?: string 8 | exp?: string 9 | ticker?: string 10 | rawNumeric?: string 11 | } -------------------------------------------------------------------------------- /src/gen/coinbase/staking/rewards/v1/portfolio.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | export type Portfolio = { 7 | name?: string 8 | addresses?: Address[] 9 | } 10 | 11 | export type Address = { 12 | address?: string 13 | } 14 | 15 | export type GetPortfolioRequest = { 16 | name?: string 17 | } 18 | 19 | export type ListPortfoliosRequest = { 20 | pageSize?: number 21 | pageToken?: string 22 | } 23 | 24 | export type ListPortfoliosResponse = { 25 | portfolios?: Portfolio[] 26 | nextPageToken?: string 27 | } -------------------------------------------------------------------------------- /src/gen/coinbase/staking/rewards/v1/protocol.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | export type Protocol = { 7 | name?: string 8 | } -------------------------------------------------------------------------------- /src/gen/coinbase/staking/rewards/v1/reward.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | import * as GoogleProtobufTimestamp from "../../../../google/protobuf/timestamp.pb" 8 | import * as CoinbaseStakingRewardsV1Common from "./common.pb" 9 | import * as CoinbaseStakingRewardsV1Stake from "./stake.pb" 10 | 11 | type Absent = { [k in Exclude]?: undefined }; 12 | type OneOf = 13 | | { [k in keyof T]?: undefined } 14 | | ( 15 | keyof T extends infer K ? 16 | (K extends string & keyof T ? { [k in K]: T[K] } & Absent 17 | : never) 18 | : never); 19 | 20 | export enum AggregationUnit { 21 | AGGREGATION_UNIT_UNSPECIFIED = "AGGREGATION_UNIT_UNSPECIFIED", 22 | EPOCH = "EPOCH", 23 | DAY = "DAY", 24 | } 25 | 26 | export enum RewardState { 27 | STATE_UNSPECIFIED = "STATE_UNSPECIFIED", 28 | PENDING_CLAIMABLE = "PENDING_CLAIMABLE", 29 | MATERIALIZED = "MATERIALIZED", 30 | } 31 | 32 | export enum USDValueSource { 33 | SOURCE_UNSPECIFIED = "SOURCE_UNSPECIFIED", 34 | COINBASE_EXCHANGE = "COINBASE_EXCHANGE", 35 | } 36 | 37 | 38 | type BaseReward = { 39 | address?: string 40 | aggregationUnit?: AggregationUnit 41 | periodStartTime?: GoogleProtobufTimestamp.Timestamp 42 | periodEndTime?: GoogleProtobufTimestamp.Timestamp 43 | totalEarnedNativeUnit?: CoinbaseStakingRewardsV1Common.AssetAmount 44 | totalEarnedUsd?: USDValue[] 45 | endingBalance?: CoinbaseStakingRewardsV1Stake.Stake 46 | protocol?: string 47 | rewardState?: RewardState 48 | } 49 | 50 | export type Reward = BaseReward 51 | & OneOf<{ epoch: string; date: string }> 52 | 53 | export type USDValue = { 54 | source?: USDValueSource 55 | conversionTime?: GoogleProtobufTimestamp.Timestamp 56 | amount?: CoinbaseStakingRewardsV1Common.AssetAmount 57 | conversionPrice?: string 58 | } 59 | 60 | export type ListRewardsRequest = { 61 | parent?: string 62 | pageSize?: number 63 | pageToken?: string 64 | filter?: string 65 | } 66 | 67 | export type ListRewardsResponse = { 68 | rewards?: Reward[] 69 | nextPageToken?: string 70 | } -------------------------------------------------------------------------------- /src/gen/coinbase/staking/rewards/v1/reward_service.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | import * as fm from "../../../../fetch.pb" 8 | import * as CoinbaseStakingRewardsV1Portfolio from "./portfolio.pb" 9 | import * as CoinbaseStakingRewardsV1Reward from "./reward.pb" 10 | import * as CoinbaseStakingRewardsV1Stake from "./stake.pb" 11 | export class RewardService { 12 | static ListRewards(req: CoinbaseStakingRewardsV1Reward.ListRewardsRequest, initReq?: fm.InitReq): Promise { 13 | return fm.fetchReq(`/v1/${req["parent"]}/rewards?${fm.renderURLSearchParams(req, ["parent"])}`, {...initReq, method: "GET"}) 14 | } 15 | static ListStakes(req: CoinbaseStakingRewardsV1Stake.ListStakesRequest, initReq?: fm.InitReq): Promise { 16 | return fm.fetchReq(`/v1/${req["parent"]}/stakes?${fm.renderURLSearchParams(req, ["parent"])}`, {...initReq, method: "GET"}) 17 | } 18 | static GetPortfolio(req: CoinbaseStakingRewardsV1Portfolio.GetPortfolioRequest, initReq?: fm.InitReq): Promise { 19 | return fm.fetchReq(`/v1/${req["nameportfolios"]}?${fm.renderURLSearchParams(req, ["nameportfolios"])}`, {...initReq, method: "GET"}) 20 | } 21 | static ListPortfolios(req: CoinbaseStakingRewardsV1Portfolio.ListPortfoliosRequest, initReq?: fm.InitReq): Promise { 22 | return fm.fetchReq(`/v1/portfolios?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"}) 23 | } 24 | } -------------------------------------------------------------------------------- /src/gen/coinbase/staking/rewards/v1/stake.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | import * as GoogleProtobufTimestamp from "../../../../google/protobuf/timestamp.pb" 8 | import * as CoinbaseStakingRewardsV1Common from "./common.pb" 9 | 10 | type Absent = { [k in Exclude]?: undefined }; 11 | type OneOf = 12 | | { [k in keyof T]?: undefined } 13 | | ( 14 | keyof T extends infer K ? 15 | (K extends string & keyof T ? { [k in K]: T[K] } & Absent 16 | : never) 17 | : never); 18 | 19 | export enum ParticipantType { 20 | PARTICIPANT_TYPE_UNSPECIFIED = "PARTICIPANT_TYPE_UNSPECIFIED", 21 | DELEGATOR = "DELEGATOR", 22 | VALIDATOR = "VALIDATOR", 23 | } 24 | 25 | export enum RewardRateCalculationMethods { 26 | CALCULATION_METHODS_UNSPECIFIED = "CALCULATION_METHODS_UNSPECIFIED", 27 | SOLO_STAKER = "SOLO_STAKER", 28 | POOLED_STAKER = "POOLED_STAKER", 29 | EPOCH_AUTO_COMPOUNDING = "EPOCH_AUTO_COMPOUNDING", 30 | NO_AUTO_COMPOUNDING = "NO_AUTO_COMPOUNDING", 31 | } 32 | 33 | export type StakeDelegation = { 34 | address?: string 35 | amount?: CoinbaseStakingRewardsV1Common.AssetAmount 36 | commissionRate?: string 37 | } 38 | 39 | 40 | type BaseStake = { 41 | address?: string 42 | evaluationTime?: GoogleProtobufTimestamp.Timestamp 43 | bondedStake?: CoinbaseStakingRewardsV1Common.AssetAmount 44 | rewardRateCalculations?: RewardRate[] 45 | participantType?: ParticipantType 46 | protocol?: string 47 | unbondedBalance?: CoinbaseStakingRewardsV1Common.AssetAmount 48 | } 49 | 50 | export type Stake = BaseStake 51 | & OneOf<{ totalDelegationReceived: CoinbaseStakingRewardsV1Common.AssetAmount }> 52 | & OneOf<{ delegationsReceived: StakeDelegation }> 53 | & OneOf<{ delegationsGiven: StakeDelegation }> 54 | 55 | export type RewardRate = { 56 | percentage?: string 57 | calculatedTime?: GoogleProtobufTimestamp.Timestamp 58 | calculationMethod?: RewardRateCalculationMethods 59 | } 60 | 61 | export type ListStakesRequest = { 62 | parent?: string 63 | pageSize?: number 64 | pageToken?: string 65 | filter?: string 66 | orderBy?: string 67 | } 68 | 69 | export type ListStakesResponse = { 70 | stakes?: Stake[] 71 | nextPageToken?: string 72 | } -------------------------------------------------------------------------------- /src/gen/fetch.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | /** 8 | * base64 encoder and decoder 9 | * Copied and adapted from https://github.com/protobufjs/protobuf.js/blob/master/lib/base64/index.js 10 | */ 11 | // Base64 encoding table 12 | const b64 = new Array(64); 13 | 14 | // Base64 decoding table 15 | const s64 = new Array(123); 16 | 17 | // 65..90, 97..122, 48..57, 43, 47 18 | for (let i = 0; i < 64;) 19 | s64[b64[i] = i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i - 59 | 43] = i++; 20 | 21 | export function b64Encode(buffer: Uint8Array, start: number, end: number): string { 22 | let parts: string[] = null; 23 | const chunk = []; 24 | let i = 0, // output index 25 | j = 0, // goto index 26 | t; // temporary 27 | while (start < end) { 28 | const b = buffer[start++]; 29 | switch (j) { 30 | case 0: 31 | chunk[i++] = b64[b >> 2]; 32 | t = (b & 3) << 4; 33 | j = 1; 34 | break; 35 | case 1: 36 | chunk[i++] = b64[t | b >> 4]; 37 | t = (b & 15) << 2; 38 | j = 2; 39 | break; 40 | case 2: 41 | chunk[i++] = b64[t | b >> 6]; 42 | chunk[i++] = b64[b & 63]; 43 | j = 0; 44 | break; 45 | } 46 | if (i > 8191) { 47 | (parts || (parts = [])).push(String.fromCharCode.apply(String, chunk)); 48 | i = 0; 49 | } 50 | } 51 | if (j) { 52 | chunk[i++] = b64[t]; 53 | chunk[i++] = 61; 54 | if (j === 1) 55 | chunk[i++] = 61; 56 | } 57 | if (parts) { 58 | if (i) 59 | parts.push(String.fromCharCode.apply(String, chunk.slice(0, i))); 60 | return parts.join(""); 61 | } 62 | return String.fromCharCode.apply(String, chunk.slice(0, i)); 63 | } 64 | 65 | const invalidEncoding = "invalid encoding"; 66 | 67 | export function b64Decode(s: string): Uint8Array { 68 | const buffer = []; 69 | let offset = 0; 70 | let j = 0, // goto index 71 | t; // temporary 72 | for (let i = 0; i < s.length;) { 73 | let c = s.charCodeAt(i++); 74 | if (c === 61 && j > 1) 75 | break; 76 | if ((c = s64[c]) === undefined) 77 | throw Error(invalidEncoding); 78 | switch (j) { 79 | case 0: 80 | t = c; 81 | j = 1; 82 | break; 83 | case 1: 84 | buffer[offset++] = t << 2 | (c & 48) >> 4; 85 | t = c; 86 | j = 2; 87 | break; 88 | case 2: 89 | buffer[offset++] = (t & 15) << 4 | (c & 60) >> 2; 90 | t = c; 91 | j = 3; 92 | break; 93 | case 3: 94 | buffer[offset++] = (t & 3) << 6 | c; 95 | j = 0; 96 | break; 97 | } 98 | } 99 | if (j === 1) 100 | throw Error(invalidEncoding); 101 | return new Uint8Array(buffer); 102 | } 103 | 104 | function b64Test(s: string): boolean { 105 | return /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(s); 106 | } 107 | 108 | export interface InitReq extends RequestInit { 109 | pathPrefix?: string 110 | } 111 | 112 | export function replacer(key: any, value: any): any { 113 | if(value && value.constructor === Uint8Array) { 114 | return b64Encode(value, 0, value.length); 115 | } 116 | 117 | return value; 118 | } 119 | 120 | export function fetchReq(path: string, init?: InitReq): Promise { 121 | const {pathPrefix, ...req} = init || {} 122 | 123 | const url = pathPrefix ? `${pathPrefix}${path}` : path 124 | 125 | return fetch(url, req).then(r => r.json().then((body: O) => { 126 | if (!r.ok) { throw body; } 127 | return body; 128 | })) as Promise 129 | } 130 | 131 | // NotifyStreamEntityArrival is a callback that will be called on streaming entity arrival 132 | export type NotifyStreamEntityArrival = (resp: T) => void 133 | 134 | /** 135 | * fetchStreamingRequest is able to handle grpc-gateway server side streaming call 136 | * it takes NotifyStreamEntityArrival that lets users respond to entity arrival during the call 137 | * all entities will be returned as an array after the call finishes. 138 | **/ 139 | export async function fetchStreamingRequest(path: string, callback?: NotifyStreamEntityArrival, init?: InitReq) { 140 | const {pathPrefix, ...req} = init || {} 141 | const url = pathPrefix ?`${pathPrefix}${path}` : path 142 | const result = await fetch(url, req) 143 | // needs to use the .ok to check the status of HTTP status code 144 | // http other than 200 will not throw an error, instead the .ok will become false. 145 | // see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch# 146 | if (!result.ok) { 147 | const resp = await result.json() 148 | const errMsg = resp.error && resp.error.message ? resp.error.message : "" 149 | throw new Error(errMsg) 150 | } 151 | 152 | if (!result.body) { 153 | throw new Error("response doesnt have a body") 154 | } 155 | 156 | await result.body 157 | .pipeThrough(new TextDecoderStream()) 158 | .pipeThrough(getNewLineDelimitedJSONDecodingStream()) 159 | .pipeTo(getNotifyEntityArrivalSink((e: R) => { 160 | if (callback) { 161 | callback(e) 162 | } 163 | })) 164 | 165 | // wait for the streaming to finish and return the success respond 166 | return 167 | } 168 | 169 | /** 170 | * JSONStringStreamController represents the transform controller that's able to transform the incoming 171 | * new line delimited json content stream into entities and able to push the entity to the down stream 172 | */ 173 | interface JSONStringStreamController extends TransformStreamDefaultController { 174 | buf?: string 175 | pos?: number 176 | enqueue: (s: T) => void 177 | } 178 | 179 | /** 180 | * getNewLineDelimitedJSONDecodingStream returns a TransformStream that's able to handle new line delimited json stream content into parsed entities 181 | */ 182 | function getNewLineDelimitedJSONDecodingStream(): TransformStream { 183 | return new TransformStream({ 184 | start(controller: JSONStringStreamController) { 185 | controller.buf = '' 186 | controller.pos = 0 187 | }, 188 | 189 | transform(chunk: string, controller: JSONStringStreamController) { 190 | if (controller.buf === undefined) { 191 | controller.buf = '' 192 | } 193 | if (controller.pos === undefined) { 194 | controller.pos = 0 195 | } 196 | controller.buf += chunk 197 | while (controller.pos < controller.buf.length) { 198 | if (controller.buf[controller.pos] === '\n') { 199 | const line = controller.buf.substring(0, controller.pos) 200 | const response = JSON.parse(line) 201 | controller.enqueue(response.result) 202 | controller.buf = controller.buf.substring(controller.pos + 1) 203 | controller.pos = 0 204 | } else { 205 | ++controller.pos 206 | } 207 | } 208 | } 209 | }) 210 | 211 | } 212 | 213 | /** 214 | * getNotifyEntityArrivalSink takes the NotifyStreamEntityArrival callback and return 215 | * a sink that will call the callback on entity arrival 216 | * @param notifyCallback 217 | */ 218 | function getNotifyEntityArrivalSink(notifyCallback: NotifyStreamEntityArrival) { 219 | return new WritableStream({ 220 | write(entity: T) { 221 | notifyCallback(entity) 222 | } 223 | }) 224 | } 225 | 226 | type Primitive = string | boolean | number; 227 | type RequestPayload = Record; 228 | type FlattenedRequestPayload = Record>; 229 | 230 | /** 231 | * Checks if given value is a plain object 232 | * Logic copied and adapted from below source: 233 | * https://github.com/char0n/ramda-adjunct/blob/master/src/isPlainObj.js 234 | * @param {unknown} value 235 | * @return {boolean} 236 | */ 237 | function isPlainObject(value: unknown): boolean { 238 | const isObject = 239 | Object.prototype.toString.call(value).slice(8, -1) === "Object"; 240 | const isObjLike = value !== null && isObject; 241 | 242 | if (!isObjLike || !isObject) { 243 | return false; 244 | } 245 | 246 | const proto = Object.getPrototypeOf(value); 247 | 248 | const hasObjectConstructor = 249 | typeof proto === "object" && 250 | proto.constructor === Object.prototype.constructor; 251 | 252 | return hasObjectConstructor; 253 | } 254 | 255 | /** 256 | * Checks if given value is of a primitive type 257 | * @param {unknown} value 258 | * @return {boolean} 259 | */ 260 | function isPrimitive(value: unknown): boolean { 261 | return ["string", "number", "boolean"].some(t => typeof value === t); 262 | } 263 | 264 | /** 265 | * Checks if given primitive is zero-value 266 | * @param {Primitive} value 267 | * @return {boolean} 268 | */ 269 | function isZeroValuePrimitive(value: Primitive): boolean { 270 | return value === false || value === 0 || value === ""; 271 | } 272 | 273 | /** 274 | * Flattens a deeply nested request payload and returns an object 275 | * with only primitive values and non-empty array of primitive values 276 | * as per https://github.com/googleapis/googleapis/blob/master/google/api/http.proto 277 | * @param {RequestPayload} requestPayload 278 | * @param {String} path 279 | * @return {FlattenedRequestPayload>} 280 | */ 281 | function flattenRequestPayload( 282 | requestPayload: T, 283 | path: string = "" 284 | ): FlattenedRequestPayload { 285 | return Object.keys(requestPayload).reduce( 286 | (acc: T, key: string): T => { 287 | const value = requestPayload[key]; 288 | const newPath = path ? [path, key].join(".") : key; 289 | 290 | const isNonEmptyPrimitiveArray = 291 | Array.isArray(value) && 292 | value.every(v => isPrimitive(v)) && 293 | value.length > 0; 294 | 295 | const isNonZeroValuePrimitive = 296 | isPrimitive(value) && !isZeroValuePrimitive(value as Primitive); 297 | 298 | let objectToMerge = {}; 299 | 300 | if (isPlainObject(value)) { 301 | objectToMerge = flattenRequestPayload(value as RequestPayload, newPath); 302 | } else if (isNonZeroValuePrimitive || isNonEmptyPrimitiveArray) { 303 | objectToMerge = { [newPath]: value }; 304 | } 305 | 306 | return { ...acc, ...objectToMerge }; 307 | }, 308 | {} as T 309 | ) as FlattenedRequestPayload; 310 | } 311 | 312 | /** 313 | * Renders a deeply nested request payload into a string of URL search 314 | * parameters by first flattening the request payload and then removing keys 315 | * which are already present in the URL path. 316 | * @param {RequestPayload} requestPayload 317 | * @param {string[]} urlPathParams 318 | * @return {string} 319 | */ 320 | export function renderURLSearchParams( 321 | requestPayload: T, 322 | urlPathParams: string[] = [] 323 | ): string { 324 | const flattenedRequestPayload = flattenRequestPayload(requestPayload); 325 | 326 | const urlSearchParams = Object.keys(flattenedRequestPayload).reduce( 327 | (acc: string[][], key: string): string[][] => { 328 | // key should not be present in the url path as a parameter 329 | const value = flattenedRequestPayload[key]; 330 | if (urlPathParams.find(f => f === key)) { 331 | return acc; 332 | } 333 | return Array.isArray(value) 334 | ? [...acc, ...value.map(m => [key, m.toString()])] 335 | : (acc = [...acc, [key, value.toString()]]); 336 | }, 337 | [] as string[][] 338 | ); 339 | 340 | return new URLSearchParams(urlSearchParams).toString(); 341 | } -------------------------------------------------------------------------------- /src/gen/google/api/annotations.pb.ts: -------------------------------------------------------------------------------- 1 | export default {} -------------------------------------------------------------------------------- /src/gen/google/api/client.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | import * as GoogleProtobufDuration from "../protobuf/duration.pb" 8 | import * as GoogleApiLaunch_stage from "./launch_stage.pb" 9 | 10 | export enum ClientLibraryOrganization { 11 | CLIENT_LIBRARY_ORGANIZATION_UNSPECIFIED = "CLIENT_LIBRARY_ORGANIZATION_UNSPECIFIED", 12 | CLOUD = "CLOUD", 13 | ADS = "ADS", 14 | PHOTOS = "PHOTOS", 15 | STREET_VIEW = "STREET_VIEW", 16 | SHOPPING = "SHOPPING", 17 | GEO = "GEO", 18 | GENERATIVE_AI = "GENERATIVE_AI", 19 | } 20 | 21 | export enum ClientLibraryDestination { 22 | CLIENT_LIBRARY_DESTINATION_UNSPECIFIED = "CLIENT_LIBRARY_DESTINATION_UNSPECIFIED", 23 | GITHUB = "GITHUB", 24 | PACKAGE_MANAGER = "PACKAGE_MANAGER", 25 | } 26 | 27 | export type CommonLanguageSettings = { 28 | referenceDocsUri?: string 29 | destinations?: ClientLibraryDestination[] 30 | } 31 | 32 | export type ClientLibrarySettings = { 33 | version?: string 34 | launchStage?: GoogleApiLaunch_stage.LaunchStage 35 | restNumericEnums?: boolean 36 | javaSettings?: JavaSettings 37 | cppSettings?: CppSettings 38 | phpSettings?: PhpSettings 39 | pythonSettings?: PythonSettings 40 | nodeSettings?: NodeSettings 41 | dotnetSettings?: DotnetSettings 42 | rubySettings?: RubySettings 43 | goSettings?: GoSettings 44 | } 45 | 46 | export type Publishing = { 47 | methodSettings?: MethodSettings[] 48 | newIssueUri?: string 49 | documentationUri?: string 50 | apiShortName?: string 51 | githubLabel?: string 52 | codeownerGithubTeams?: string[] 53 | docTagPrefix?: string 54 | organization?: ClientLibraryOrganization 55 | librarySettings?: ClientLibrarySettings[] 56 | protoReferenceDocumentationUri?: string 57 | restReferenceDocumentationUri?: string 58 | } 59 | 60 | export type JavaSettings = { 61 | libraryPackage?: string 62 | serviceClassNames?: {[key: string]: string} 63 | common?: CommonLanguageSettings 64 | } 65 | 66 | export type CppSettings = { 67 | common?: CommonLanguageSettings 68 | } 69 | 70 | export type PhpSettings = { 71 | common?: CommonLanguageSettings 72 | } 73 | 74 | export type PythonSettings = { 75 | common?: CommonLanguageSettings 76 | } 77 | 78 | export type NodeSettings = { 79 | common?: CommonLanguageSettings 80 | } 81 | 82 | export type DotnetSettings = { 83 | common?: CommonLanguageSettings 84 | renamedServices?: {[key: string]: string} 85 | renamedResources?: {[key: string]: string} 86 | ignoredResources?: string[] 87 | forcedNamespaceAliases?: string[] 88 | handwrittenSignatures?: string[] 89 | } 90 | 91 | export type RubySettings = { 92 | common?: CommonLanguageSettings 93 | } 94 | 95 | export type GoSettings = { 96 | common?: CommonLanguageSettings 97 | } 98 | 99 | export type MethodSettingsLongRunning = { 100 | initialPollDelay?: GoogleProtobufDuration.Duration 101 | pollDelayMultiplier?: number 102 | maxPollDelay?: GoogleProtobufDuration.Duration 103 | totalPollTimeout?: GoogleProtobufDuration.Duration 104 | } 105 | 106 | export type MethodSettings = { 107 | selector?: string 108 | longRunning?: MethodSettingsLongRunning 109 | autoPopulatedFields?: string[] 110 | } -------------------------------------------------------------------------------- /src/gen/google/api/field_behavior.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | export enum FieldBehavior { 8 | FIELD_BEHAVIOR_UNSPECIFIED = "FIELD_BEHAVIOR_UNSPECIFIED", 9 | OPTIONAL = "OPTIONAL", 10 | REQUIRED = "REQUIRED", 11 | OUTPUT_ONLY = "OUTPUT_ONLY", 12 | INPUT_ONLY = "INPUT_ONLY", 13 | IMMUTABLE = "IMMUTABLE", 14 | UNORDERED_LIST = "UNORDERED_LIST", 15 | NON_EMPTY_DEFAULT = "NON_EMPTY_DEFAULT", 16 | IDENTIFIER = "IDENTIFIER", 17 | } -------------------------------------------------------------------------------- /src/gen/google/api/http.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | type Absent = { [k in Exclude]?: undefined }; 8 | type OneOf = 9 | | { [k in keyof T]?: undefined } 10 | | ( 11 | keyof T extends infer K ? 12 | (K extends string & keyof T ? { [k in K]: T[K] } & Absent 13 | : never) 14 | : never); 15 | export type Http = { 16 | rules?: HttpRule[] 17 | fullyDecodeReservedExpansion?: boolean 18 | } 19 | 20 | 21 | type BaseHttpRule = { 22 | selector?: string 23 | body?: string 24 | responseBody?: string 25 | additionalBindings?: HttpRule[] 26 | } 27 | 28 | export type HttpRule = BaseHttpRule 29 | & OneOf<{ get: string; put: string; post: string; delete: string; patch: string; custom: CustomHttpPattern }> 30 | 31 | export type CustomHttpPattern = { 32 | kind?: string 33 | path?: string 34 | } -------------------------------------------------------------------------------- /src/gen/google/api/launch_stage.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | export enum LaunchStage { 8 | LAUNCH_STAGE_UNSPECIFIED = "LAUNCH_STAGE_UNSPECIFIED", 9 | UNIMPLEMENTED = "UNIMPLEMENTED", 10 | PRELAUNCH = "PRELAUNCH", 11 | EARLY_ACCESS = "EARLY_ACCESS", 12 | ALPHA = "ALPHA", 13 | BETA = "BETA", 14 | GA = "GA", 15 | DEPRECATED = "DEPRECATED", 16 | } -------------------------------------------------------------------------------- /src/gen/google/api/resource.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | export enum ResourceDescriptorHistory { 8 | HISTORY_UNSPECIFIED = "HISTORY_UNSPECIFIED", 9 | ORIGINALLY_SINGLE_PATTERN = "ORIGINALLY_SINGLE_PATTERN", 10 | FUTURE_MULTI_PATTERN = "FUTURE_MULTI_PATTERN", 11 | } 12 | 13 | export enum ResourceDescriptorStyle { 14 | STYLE_UNSPECIFIED = "STYLE_UNSPECIFIED", 15 | DECLARATIVE_FRIENDLY = "DECLARATIVE_FRIENDLY", 16 | } 17 | 18 | export type ResourceDescriptor = { 19 | type?: string 20 | pattern?: string[] 21 | nameField?: string 22 | history?: ResourceDescriptorHistory 23 | plural?: string 24 | singular?: string 25 | style?: ResourceDescriptorStyle[] 26 | } 27 | 28 | export type ResourceReference = { 29 | type?: string 30 | childType?: string 31 | } -------------------------------------------------------------------------------- /src/gen/google/protobuf/descriptor.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | export enum Edition { 8 | EDITION_UNKNOWN = "EDITION_UNKNOWN", 9 | EDITION_PROTO2 = "EDITION_PROTO2", 10 | EDITION_PROTO3 = "EDITION_PROTO3", 11 | EDITION_2023 = "EDITION_2023", 12 | EDITION_1_TEST_ONLY = "EDITION_1_TEST_ONLY", 13 | EDITION_2_TEST_ONLY = "EDITION_2_TEST_ONLY", 14 | EDITION_99997_TEST_ONLY = "EDITION_99997_TEST_ONLY", 15 | EDITION_99998_TEST_ONLY = "EDITION_99998_TEST_ONLY", 16 | EDITION_99999_TEST_ONLY = "EDITION_99999_TEST_ONLY", 17 | } 18 | 19 | export enum ExtensionRangeOptionsVerificationState { 20 | DECLARATION = "DECLARATION", 21 | UNVERIFIED = "UNVERIFIED", 22 | } 23 | 24 | export enum FieldDescriptorProtoType { 25 | TYPE_DOUBLE = "TYPE_DOUBLE", 26 | TYPE_FLOAT = "TYPE_FLOAT", 27 | TYPE_INT64 = "TYPE_INT64", 28 | TYPE_UINT64 = "TYPE_UINT64", 29 | TYPE_INT32 = "TYPE_INT32", 30 | TYPE_FIXED64 = "TYPE_FIXED64", 31 | TYPE_FIXED32 = "TYPE_FIXED32", 32 | TYPE_BOOL = "TYPE_BOOL", 33 | TYPE_STRING = "TYPE_STRING", 34 | TYPE_GROUP = "TYPE_GROUP", 35 | TYPE_MESSAGE = "TYPE_MESSAGE", 36 | TYPE_BYTES = "TYPE_BYTES", 37 | TYPE_UINT32 = "TYPE_UINT32", 38 | TYPE_ENUM = "TYPE_ENUM", 39 | TYPE_SFIXED32 = "TYPE_SFIXED32", 40 | TYPE_SFIXED64 = "TYPE_SFIXED64", 41 | TYPE_SINT32 = "TYPE_SINT32", 42 | TYPE_SINT64 = "TYPE_SINT64", 43 | } 44 | 45 | export enum FieldDescriptorProtoLabel { 46 | LABEL_OPTIONAL = "LABEL_OPTIONAL", 47 | LABEL_REPEATED = "LABEL_REPEATED", 48 | LABEL_REQUIRED = "LABEL_REQUIRED", 49 | } 50 | 51 | export enum FileOptionsOptimizeMode { 52 | SPEED = "SPEED", 53 | CODE_SIZE = "CODE_SIZE", 54 | LITE_RUNTIME = "LITE_RUNTIME", 55 | } 56 | 57 | export enum FieldOptionsCType { 58 | STRING = "STRING", 59 | CORD = "CORD", 60 | STRING_PIECE = "STRING_PIECE", 61 | } 62 | 63 | export enum FieldOptionsJSType { 64 | JS_NORMAL = "JS_NORMAL", 65 | JS_STRING = "JS_STRING", 66 | JS_NUMBER = "JS_NUMBER", 67 | } 68 | 69 | export enum FieldOptionsOptionRetention { 70 | RETENTION_UNKNOWN = "RETENTION_UNKNOWN", 71 | RETENTION_RUNTIME = "RETENTION_RUNTIME", 72 | RETENTION_SOURCE = "RETENTION_SOURCE", 73 | } 74 | 75 | export enum FieldOptionsOptionTargetType { 76 | TARGET_TYPE_UNKNOWN = "TARGET_TYPE_UNKNOWN", 77 | TARGET_TYPE_FILE = "TARGET_TYPE_FILE", 78 | TARGET_TYPE_EXTENSION_RANGE = "TARGET_TYPE_EXTENSION_RANGE", 79 | TARGET_TYPE_MESSAGE = "TARGET_TYPE_MESSAGE", 80 | TARGET_TYPE_FIELD = "TARGET_TYPE_FIELD", 81 | TARGET_TYPE_ONEOF = "TARGET_TYPE_ONEOF", 82 | TARGET_TYPE_ENUM = "TARGET_TYPE_ENUM", 83 | TARGET_TYPE_ENUM_ENTRY = "TARGET_TYPE_ENUM_ENTRY", 84 | TARGET_TYPE_SERVICE = "TARGET_TYPE_SERVICE", 85 | TARGET_TYPE_METHOD = "TARGET_TYPE_METHOD", 86 | } 87 | 88 | export enum MethodOptionsIdempotencyLevel { 89 | IDEMPOTENCY_UNKNOWN = "IDEMPOTENCY_UNKNOWN", 90 | NO_SIDE_EFFECTS = "NO_SIDE_EFFECTS", 91 | IDEMPOTENT = "IDEMPOTENT", 92 | } 93 | 94 | export enum FeatureSetFieldPresence { 95 | FIELD_PRESENCE_UNKNOWN = "FIELD_PRESENCE_UNKNOWN", 96 | EXPLICIT = "EXPLICIT", 97 | IMPLICIT = "IMPLICIT", 98 | LEGACY_REQUIRED = "LEGACY_REQUIRED", 99 | } 100 | 101 | export enum FeatureSetEnumType { 102 | ENUM_TYPE_UNKNOWN = "ENUM_TYPE_UNKNOWN", 103 | OPEN = "OPEN", 104 | CLOSED = "CLOSED", 105 | } 106 | 107 | export enum FeatureSetRepeatedFieldEncoding { 108 | REPEATED_FIELD_ENCODING_UNKNOWN = "REPEATED_FIELD_ENCODING_UNKNOWN", 109 | PACKED = "PACKED", 110 | EXPANDED = "EXPANDED", 111 | } 112 | 113 | export enum FeatureSetUtf8Validation { 114 | UTF8_VALIDATION_UNKNOWN = "UTF8_VALIDATION_UNKNOWN", 115 | NONE = "NONE", 116 | VERIFY = "VERIFY", 117 | } 118 | 119 | export enum FeatureSetMessageEncoding { 120 | MESSAGE_ENCODING_UNKNOWN = "MESSAGE_ENCODING_UNKNOWN", 121 | LENGTH_PREFIXED = "LENGTH_PREFIXED", 122 | DELIMITED = "DELIMITED", 123 | } 124 | 125 | export enum FeatureSetJsonFormat { 126 | JSON_FORMAT_UNKNOWN = "JSON_FORMAT_UNKNOWN", 127 | ALLOW = "ALLOW", 128 | LEGACY_BEST_EFFORT = "LEGACY_BEST_EFFORT", 129 | } 130 | 131 | export enum GeneratedCodeInfoAnnotationSemantic { 132 | NONE = "NONE", 133 | SET = "SET", 134 | ALIAS = "ALIAS", 135 | } 136 | 137 | export type FileDescriptorSet = { 138 | file?: FileDescriptorProto[] 139 | } 140 | 141 | export type FileDescriptorProto = { 142 | name?: string 143 | package?: string 144 | dependency?: string[] 145 | publicDependency?: number[] 146 | weakDependency?: number[] 147 | messageType?: DescriptorProto[] 148 | enumType?: EnumDescriptorProto[] 149 | service?: ServiceDescriptorProto[] 150 | extension?: FieldDescriptorProto[] 151 | options?: FileOptions 152 | sourceCodeInfo?: SourceCodeInfo 153 | syntax?: string 154 | edition?: Edition 155 | } 156 | 157 | export type DescriptorProtoExtensionRange = { 158 | start?: number 159 | end?: number 160 | options?: ExtensionRangeOptions 161 | } 162 | 163 | export type DescriptorProtoReservedRange = { 164 | start?: number 165 | end?: number 166 | } 167 | 168 | export type DescriptorProto = { 169 | name?: string 170 | field?: FieldDescriptorProto[] 171 | extension?: FieldDescriptorProto[] 172 | nestedType?: DescriptorProto[] 173 | enumType?: EnumDescriptorProto[] 174 | extensionRange?: DescriptorProtoExtensionRange[] 175 | oneofDecl?: OneofDescriptorProto[] 176 | options?: MessageOptions 177 | reservedRange?: DescriptorProtoReservedRange[] 178 | reservedName?: string[] 179 | } 180 | 181 | export type ExtensionRangeOptionsDeclaration = { 182 | number?: number 183 | fullName?: string 184 | type?: string 185 | reserved?: boolean 186 | repeated?: boolean 187 | } 188 | 189 | export type ExtensionRangeOptions = { 190 | uninterpretedOption?: UninterpretedOption[] 191 | declaration?: ExtensionRangeOptionsDeclaration[] 192 | features?: FeatureSet 193 | verification?: ExtensionRangeOptionsVerificationState 194 | } 195 | 196 | export type FieldDescriptorProto = { 197 | name?: string 198 | number?: number 199 | label?: FieldDescriptorProtoLabel 200 | type?: FieldDescriptorProtoType 201 | typeName?: string 202 | extendee?: string 203 | defaultValue?: string 204 | oneofIndex?: number 205 | jsonName?: string 206 | options?: FieldOptions 207 | proto3Optional?: boolean 208 | } 209 | 210 | export type OneofDescriptorProto = { 211 | name?: string 212 | options?: OneofOptions 213 | } 214 | 215 | export type EnumDescriptorProtoEnumReservedRange = { 216 | start?: number 217 | end?: number 218 | } 219 | 220 | export type EnumDescriptorProto = { 221 | name?: string 222 | value?: EnumValueDescriptorProto[] 223 | options?: EnumOptions 224 | reservedRange?: EnumDescriptorProtoEnumReservedRange[] 225 | reservedName?: string[] 226 | } 227 | 228 | export type EnumValueDescriptorProto = { 229 | name?: string 230 | number?: number 231 | options?: EnumValueOptions 232 | } 233 | 234 | export type ServiceDescriptorProto = { 235 | name?: string 236 | method?: MethodDescriptorProto[] 237 | options?: ServiceOptions 238 | } 239 | 240 | export type MethodDescriptorProto = { 241 | name?: string 242 | inputType?: string 243 | outputType?: string 244 | options?: MethodOptions 245 | clientStreaming?: boolean 246 | serverStreaming?: boolean 247 | } 248 | 249 | export type FileOptions = { 250 | javaPackage?: string 251 | javaOuterClassname?: string 252 | javaMultipleFiles?: boolean 253 | javaGenerateEqualsAndHash?: boolean 254 | javaStringCheckUtf8?: boolean 255 | optimizeFor?: FileOptionsOptimizeMode 256 | goPackage?: string 257 | ccGenericServices?: boolean 258 | javaGenericServices?: boolean 259 | pyGenericServices?: boolean 260 | phpGenericServices?: boolean 261 | deprecated?: boolean 262 | ccEnableArenas?: boolean 263 | objcClassPrefix?: string 264 | csharpNamespace?: string 265 | swiftPrefix?: string 266 | phpClassPrefix?: string 267 | phpNamespace?: string 268 | phpMetadataNamespace?: string 269 | rubyPackage?: string 270 | features?: FeatureSet 271 | uninterpretedOption?: UninterpretedOption[] 272 | } 273 | 274 | export type MessageOptions = { 275 | messageSetWireFormat?: boolean 276 | noStandardDescriptorAccessor?: boolean 277 | deprecated?: boolean 278 | mapEntry?: boolean 279 | deprecatedLegacyJsonFieldConflicts?: boolean 280 | features?: FeatureSet 281 | uninterpretedOption?: UninterpretedOption[] 282 | } 283 | 284 | export type FieldOptionsEditionDefault = { 285 | edition?: Edition 286 | value?: string 287 | } 288 | 289 | export type FieldOptions = { 290 | ctype?: FieldOptionsCType 291 | packed?: boolean 292 | jstype?: FieldOptionsJSType 293 | lazy?: boolean 294 | unverifiedLazy?: boolean 295 | deprecated?: boolean 296 | weak?: boolean 297 | debugRedact?: boolean 298 | retention?: FieldOptionsOptionRetention 299 | targets?: FieldOptionsOptionTargetType[] 300 | editionDefaults?: FieldOptionsEditionDefault[] 301 | features?: FeatureSet 302 | uninterpretedOption?: UninterpretedOption[] 303 | } 304 | 305 | export type OneofOptions = { 306 | features?: FeatureSet 307 | uninterpretedOption?: UninterpretedOption[] 308 | } 309 | 310 | export type EnumOptions = { 311 | allowAlias?: boolean 312 | deprecated?: boolean 313 | deprecatedLegacyJsonFieldConflicts?: boolean 314 | features?: FeatureSet 315 | uninterpretedOption?: UninterpretedOption[] 316 | } 317 | 318 | export type EnumValueOptions = { 319 | deprecated?: boolean 320 | features?: FeatureSet 321 | debugRedact?: boolean 322 | uninterpretedOption?: UninterpretedOption[] 323 | } 324 | 325 | export type ServiceOptions = { 326 | features?: FeatureSet 327 | deprecated?: boolean 328 | uninterpretedOption?: UninterpretedOption[] 329 | } 330 | 331 | export type MethodOptions = { 332 | deprecated?: boolean 333 | idempotencyLevel?: MethodOptionsIdempotencyLevel 334 | features?: FeatureSet 335 | uninterpretedOption?: UninterpretedOption[] 336 | } 337 | 338 | export type UninterpretedOptionNamePart = { 339 | namePart?: string 340 | isExtension?: boolean 341 | } 342 | 343 | export type UninterpretedOption = { 344 | name?: UninterpretedOptionNamePart[] 345 | identifierValue?: string 346 | positiveIntValue?: string 347 | negativeIntValue?: string 348 | doubleValue?: number 349 | stringValue?: Uint8Array 350 | aggregateValue?: string 351 | } 352 | 353 | export type FeatureSet = { 354 | fieldPresence?: FeatureSetFieldPresence 355 | enumType?: FeatureSetEnumType 356 | repeatedFieldEncoding?: FeatureSetRepeatedFieldEncoding 357 | utf8Validation?: FeatureSetUtf8Validation 358 | messageEncoding?: FeatureSetMessageEncoding 359 | jsonFormat?: FeatureSetJsonFormat 360 | } 361 | 362 | export type FeatureSetDefaultsFeatureSetEditionDefault = { 363 | edition?: Edition 364 | features?: FeatureSet 365 | } 366 | 367 | export type FeatureSetDefaults = { 368 | defaults?: FeatureSetDefaultsFeatureSetEditionDefault[] 369 | minimumEdition?: Edition 370 | maximumEdition?: Edition 371 | } 372 | 373 | export type SourceCodeInfoLocation = { 374 | path?: number[] 375 | span?: number[] 376 | leadingComments?: string 377 | trailingComments?: string 378 | leadingDetachedComments?: string[] 379 | } 380 | 381 | export type SourceCodeInfo = { 382 | location?: SourceCodeInfoLocation[] 383 | } 384 | 385 | export type GeneratedCodeInfoAnnotation = { 386 | path?: number[] 387 | sourceFile?: string 388 | begin?: number 389 | end?: number 390 | semantic?: GeneratedCodeInfoAnnotationSemantic 391 | } 392 | 393 | export type GeneratedCodeInfo = { 394 | annotation?: GeneratedCodeInfoAnnotation[] 395 | } -------------------------------------------------------------------------------- /src/gen/google/protobuf/duration.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | export type Duration = { 7 | seconds?: string 8 | nanos?: number 9 | } -------------------------------------------------------------------------------- /src/gen/google/protobuf/struct.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | type Absent = { [k in Exclude]?: undefined }; 8 | type OneOf = 9 | | { [k in keyof T]?: undefined } 10 | | ( 11 | keyof T extends infer K ? 12 | (K extends string & keyof T ? { [k in K]: T[K] } & Absent 13 | : never) 14 | : never); 15 | 16 | export enum NullValue { 17 | NULL_VALUE = "NULL_VALUE", 18 | } 19 | 20 | export type Struct = { 21 | fields?: {[key: string]: Value} 22 | } 23 | 24 | 25 | type BaseValue = { 26 | } 27 | 28 | export type Value = BaseValue 29 | & OneOf<{ nullValue: NullValue; numberValue: number; stringValue: string; boolValue: boolean; structValue: Struct; listValue: ListValue }> 30 | 31 | export type ListValue = { 32 | values?: Value[] 33 | } -------------------------------------------------------------------------------- /src/gen/google/protobuf/timestamp.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | export type Timestamp = { 7 | seconds?: string 8 | nanos?: number 9 | } -------------------------------------------------------------------------------- /src/gen/protoc-gen-openapiv2/options/annotations.pb.ts: -------------------------------------------------------------------------------- 1 | export default {} -------------------------------------------------------------------------------- /src/gen/protoc-gen-openapiv2/options/openapiv2.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | /* 4 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 5 | */ 6 | 7 | import * as GoogleProtobufStruct from "../../google/protobuf/struct.pb" 8 | 9 | export enum Scheme { 10 | UNKNOWN = "UNKNOWN", 11 | HTTP = "HTTP", 12 | HTTPS = "HTTPS", 13 | WS = "WS", 14 | WSS = "WSS", 15 | } 16 | 17 | export enum HeaderParameterType { 18 | UNKNOWN = "UNKNOWN", 19 | STRING = "STRING", 20 | NUMBER = "NUMBER", 21 | INTEGER = "INTEGER", 22 | BOOLEAN = "BOOLEAN", 23 | } 24 | 25 | export enum JSONSchemaJSONSchemaSimpleTypes { 26 | UNKNOWN = "UNKNOWN", 27 | ARRAY = "ARRAY", 28 | BOOLEAN = "BOOLEAN", 29 | INTEGER = "INTEGER", 30 | NULL = "NULL", 31 | NUMBER = "NUMBER", 32 | OBJECT = "OBJECT", 33 | STRING = "STRING", 34 | } 35 | 36 | export enum SecuritySchemeType { 37 | TYPE_INVALID = "TYPE_INVALID", 38 | TYPE_BASIC = "TYPE_BASIC", 39 | TYPE_API_KEY = "TYPE_API_KEY", 40 | TYPE_OAUTH2 = "TYPE_OAUTH2", 41 | } 42 | 43 | export enum SecuritySchemeIn { 44 | IN_INVALID = "IN_INVALID", 45 | IN_QUERY = "IN_QUERY", 46 | IN_HEADER = "IN_HEADER", 47 | } 48 | 49 | export enum SecuritySchemeFlow { 50 | FLOW_INVALID = "FLOW_INVALID", 51 | FLOW_IMPLICIT = "FLOW_IMPLICIT", 52 | FLOW_PASSWORD = "FLOW_PASSWORD", 53 | FLOW_APPLICATION = "FLOW_APPLICATION", 54 | FLOW_ACCESS_CODE = "FLOW_ACCESS_CODE", 55 | } 56 | 57 | export type Swagger = { 58 | swagger?: string 59 | info?: Info 60 | host?: string 61 | basePath?: string 62 | schemes?: Scheme[] 63 | consumes?: string[] 64 | produces?: string[] 65 | responses?: {[key: string]: Response} 66 | securityDefinitions?: SecurityDefinitions 67 | security?: SecurityRequirement[] 68 | tags?: Tag[] 69 | externalDocs?: ExternalDocumentation 70 | extensions?: {[key: string]: GoogleProtobufStruct.Value} 71 | } 72 | 73 | export type Operation = { 74 | tags?: string[] 75 | summary?: string 76 | description?: string 77 | externalDocs?: ExternalDocumentation 78 | operationId?: string 79 | consumes?: string[] 80 | produces?: string[] 81 | responses?: {[key: string]: Response} 82 | schemes?: Scheme[] 83 | deprecated?: boolean 84 | security?: SecurityRequirement[] 85 | extensions?: {[key: string]: GoogleProtobufStruct.Value} 86 | parameters?: Parameters 87 | } 88 | 89 | export type Parameters = { 90 | headers?: HeaderParameter[] 91 | } 92 | 93 | export type HeaderParameter = { 94 | name?: string 95 | description?: string 96 | type?: HeaderParameterType 97 | format?: string 98 | required?: boolean 99 | } 100 | 101 | export type Header = { 102 | description?: string 103 | type?: string 104 | format?: string 105 | default?: string 106 | pattern?: string 107 | } 108 | 109 | export type Response = { 110 | description?: string 111 | schema?: Schema 112 | headers?: {[key: string]: Header} 113 | examples?: {[key: string]: string} 114 | extensions?: {[key: string]: GoogleProtobufStruct.Value} 115 | } 116 | 117 | export type Info = { 118 | title?: string 119 | description?: string 120 | termsOfService?: string 121 | contact?: Contact 122 | license?: License 123 | version?: string 124 | extensions?: {[key: string]: GoogleProtobufStruct.Value} 125 | } 126 | 127 | export type Contact = { 128 | name?: string 129 | url?: string 130 | email?: string 131 | } 132 | 133 | export type License = { 134 | name?: string 135 | url?: string 136 | } 137 | 138 | export type ExternalDocumentation = { 139 | description?: string 140 | url?: string 141 | } 142 | 143 | export type Schema = { 144 | jsonSchema?: JSONSchema 145 | discriminator?: string 146 | readOnly?: boolean 147 | externalDocs?: ExternalDocumentation 148 | example?: string 149 | } 150 | 151 | export type JSONSchemaFieldConfiguration = { 152 | pathParamName?: string 153 | } 154 | 155 | export type JSONSchema = { 156 | ref?: string 157 | title?: string 158 | description?: string 159 | default?: string 160 | readOnly?: boolean 161 | example?: string 162 | multipleOf?: number 163 | maximum?: number 164 | exclusiveMaximum?: boolean 165 | minimum?: number 166 | exclusiveMinimum?: boolean 167 | maxLength?: string 168 | minLength?: string 169 | pattern?: string 170 | maxItems?: string 171 | minItems?: string 172 | uniqueItems?: boolean 173 | maxProperties?: string 174 | minProperties?: string 175 | required?: string[] 176 | array?: string[] 177 | type?: JSONSchemaJSONSchemaSimpleTypes[] 178 | format?: string 179 | enum?: string[] 180 | fieldConfiguration?: JSONSchemaFieldConfiguration 181 | extensions?: {[key: string]: GoogleProtobufStruct.Value} 182 | } 183 | 184 | export type Tag = { 185 | name?: string 186 | description?: string 187 | externalDocs?: ExternalDocumentation 188 | extensions?: {[key: string]: GoogleProtobufStruct.Value} 189 | } 190 | 191 | export type SecurityDefinitions = { 192 | security?: {[key: string]: SecurityScheme} 193 | } 194 | 195 | export type SecurityScheme = { 196 | type?: SecuritySchemeType 197 | description?: string 198 | name?: string 199 | in?: SecuritySchemeIn 200 | flow?: SecuritySchemeFlow 201 | authorizationUrl?: string 202 | tokenUrl?: string 203 | scopes?: Scopes 204 | extensions?: {[key: string]: GoogleProtobufStruct.Value} 205 | } 206 | 207 | export type SecurityRequirementSecurityRequirementValue = { 208 | scope?: string[] 209 | } 210 | 211 | export type SecurityRequirement = { 212 | securityRequirement?: {[key: string]: SecurityRequirementSecurityRequirementValue} 213 | } 214 | 215 | export type Scopes = { 216 | scope?: {[key: string]: string} 217 | } -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | describe('SanityCheck', () => { 4 | it('should return true', () => { 5 | expect(true).to.be.true; 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as Utils from './utils/date'; 2 | 3 | export { Utils }; 4 | 5 | export { 6 | StakingClient, 7 | workflowHasFinished, 8 | workflowWaitingForExternalBroadcast, 9 | isTxStepOutput, 10 | isWaitStepOutput, 11 | getUnsignedTx, 12 | } from './client/staking-client'; 13 | 14 | export { TxSignerFactory } from './signers'; 15 | -------------------------------------------------------------------------------- /src/signers/ethereum-signer.ts: -------------------------------------------------------------------------------- 1 | import { TransactionFactory } from '@ethereumjs/tx'; 2 | import { bytesToHex } from '@ethereumjs/util'; 3 | import { TxSigner } from './txsigner'; 4 | 5 | export class EthereumTransactionSigner implements TxSigner { 6 | async signTransaction( 7 | privateKey: string, 8 | unsignedTx: string, 9 | ): Promise { 10 | let transaction = TransactionFactory.fromSerializedData( 11 | Buffer.from(unsignedTx, 'hex'), 12 | ); 13 | 14 | const decodedPrivateKey = Buffer.from(privateKey, 'hex'); 15 | 16 | let signedTx = transaction.sign(decodedPrivateKey); 17 | 18 | const verifiedSignature = signedTx.verifySignature(); 19 | 20 | if (!verifiedSignature) { 21 | throw new Error('Produced an invalid signature!'); 22 | } 23 | 24 | return bytesToHex(signedTx.serialize()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/signers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ethereum-signer'; 2 | export * from './txsigner'; 3 | -------------------------------------------------------------------------------- /src/signers/solana-signer.ts: -------------------------------------------------------------------------------- 1 | import { Transaction, Account } from '@solana/web3.js'; 2 | import * as bs58 from 'bs58'; 3 | import { TxSigner } from './txsigner'; 4 | 5 | const TRANSACTION_SERIALIZE_CONFIG = { 6 | requireAllSignatures: false, 7 | verifySignatures: true, 8 | }; 9 | 10 | export class SolanaTransactionSigner implements TxSigner { 11 | async signTransaction( 12 | privateKey: string, 13 | unsignedTx: string, 14 | ): Promise { 15 | const txn = bs58.decode(unsignedTx); 16 | const tx = Transaction.from(txn); 17 | 18 | const secretKey = bs58.decode(privateKey); 19 | 20 | const sender = new Account(secretKey); 21 | 22 | tx.sign(sender); 23 | 24 | return bs58.encode(tx.serialize(TRANSACTION_SERIALIZE_CONFIG)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/signers/txsigner.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { TxSignerFactory } from './txsigner'; 3 | import { EthereumTransactionSigner } from './ethereum-signer'; 4 | import { SolanaTransactionSigner } from './solana-signer'; 5 | 6 | describe('TxSignerFactory', () => { 7 | it('should return an EthereumTransactionSigner for the "ethereum" protocol', () => { 8 | const signer = TxSignerFactory.getSigner('ethereum'); 9 | 10 | expect(signer).to.be.instanceOf(EthereumTransactionSigner); 11 | }); 12 | 13 | it('should return a SolanaTransactionSigner for the "solana" protocol', () => { 14 | const signer = TxSignerFactory.getSigner('solana'); 15 | 16 | expect(signer).to.be.instanceOf(SolanaTransactionSigner); 17 | }); 18 | 19 | it('should throw an error for an unsupported protocol', () => { 20 | expect(() => TxSignerFactory.getSigner('unsupported')).to.throw( 21 | 'Unsupported protocol', 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/signers/txsigner.ts: -------------------------------------------------------------------------------- 1 | import { EthereumTransactionSigner } from './ethereum-signer'; 2 | import { SolanaTransactionSigner } from './solana-signer'; 3 | 4 | export interface TxSigner { 5 | // eslint-disable-next-line no-unused-vars 6 | signTransaction(privateKey: string, unsignedTx: string): Promise; 7 | } 8 | 9 | export class TxSignerFactory { 10 | static getSigner(protocol: string): TxSigner { 11 | switch (protocol) { 12 | case 'ethereum': 13 | return new EthereumTransactionSigner(); 14 | case 'solana': 15 | return new SolanaTransactionSigner(); 16 | // other cases for additional protocols 17 | default: 18 | throw new Error('Unsupported protocol'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/date.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { calculateTimeDifference } from './date'; 3 | 4 | describe('DateUtil', () => { 5 | it('should return the correct difference in seconds for two timestamps', () => { 6 | const timestamp1 = '2023-01-01T00:00:00Z'; 7 | const timestamp2 = '2023-01-01T00:00:10Z'; 8 | const difference = calculateTimeDifference(timestamp1, timestamp2); 9 | 10 | expect(difference).to.equal(10); 11 | }); 12 | 13 | it('should return 0 for identical timestamps', () => { 14 | const timestamp1 = '2023-01-01T00:00:00Z'; 15 | const timestamp2 = '2023-01-01T00:00:00Z'; 16 | const difference = calculateTimeDifference(timestamp1, timestamp2); 17 | 18 | expect(difference).to.equal(0); 19 | }); 20 | 21 | it('should return the correct difference in seconds for timestamps in different formats', () => { 22 | const timestamp1 = '2023-01-01T00:00:00Z'; 23 | const timestamp2 = '2023-01-01T00:01:00+00:00'; 24 | const difference = calculateTimeDifference(timestamp1, timestamp2); 25 | 26 | expect(difference).to.equal(60); 27 | }); 28 | 29 | it('should handle timestamps with different time zones', () => { 30 | const timestamp1 = '2023-01-01T00:00:00Z'; 31 | const timestamp2 = '2023-01-01T01:00:00+01:00'; 32 | const difference = calculateTimeDifference(timestamp1, timestamp2); 33 | 34 | expect(difference).to.equal(0); 35 | }); 36 | 37 | it('should handle timestamps with milliseconds', () => { 38 | const timestamp1 = '2023-01-01T00:00:00.000Z'; 39 | const timestamp2 = '2023-01-01T00:00:01.500Z'; 40 | const difference = calculateTimeDifference(timestamp1, timestamp2); 41 | 42 | expect(difference).to.equal(1.5); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/utils/date.ts: -------------------------------------------------------------------------------- 1 | // calculateTimeDifference is a function that takes two timestamps and returns the difference in seconds. 2 | export function calculateTimeDifference( 3 | timestamp1: string, 4 | timestamp2: string, 5 | ): number { 6 | const date1 = new Date(timestamp1); 7 | const date2 = new Date(timestamp2); 8 | 9 | return Math.abs(date1.getTime() - date2.getTime()) / 1000; 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 4 | "module": "CommonJS", /* Specify what module code is generated. */ 5 | "moduleResolution": "node", 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 11 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 12 | "strict": true, /* Enable all strict type-checking options. */ 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "alwaysStrict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 21 | "types": [ 22 | "mocha", 23 | "node" 24 | ] 25 | }, 26 | "include": [ 27 | "src/**/*.ts" 28 | ], 29 | "exclude": [ 30 | "node_modules", 31 | "openapi", 32 | "**/*.test.ts" 33 | ] 34 | } 35 | --------------------------------------------------------------------------------