├── .github ├── CODEOWNERS ├── dependabot.yaml └── workflows │ ├── lint.yaml │ ├── main.yaml │ └── test.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── eslint.config.mjs ├── jest.config.json ├── package.json ├── proto └── tm2 │ ├── abci.proto │ └── tx.proto ├── scripts └── generate.sh ├── src ├── index.ts ├── proto │ ├── google │ │ └── protobuf │ │ │ └── any.ts │ ├── index.ts │ └── tm2 │ │ ├── abci.ts │ │ └── tx.ts ├── provider │ ├── endpoints.ts │ ├── errors │ │ ├── errors.ts │ │ ├── index.ts │ │ └── messages.ts │ ├── index.ts │ ├── jsonrpc │ │ ├── index.ts │ │ ├── jsonrpc.test.ts │ │ └── jsonrpc.ts │ ├── provider.ts │ ├── types │ │ ├── abci.ts │ │ ├── common.ts │ │ ├── index.ts │ │ └── jsonrpc.ts │ ├── utility │ │ ├── errors.utility.ts │ │ ├── index.ts │ │ ├── provider.utility.ts │ │ └── requests.utility.ts │ └── websocket │ │ ├── index.ts │ │ ├── ws.test.ts │ │ └── ws.ts ├── services │ ├── index.ts │ └── rest │ │ ├── restService.ts │ │ └── restService.types.ts └── wallet │ ├── index.ts │ ├── key │ ├── index.ts │ ├── key.test.ts │ └── key.ts │ ├── ledger │ ├── index.ts │ └── ledger.ts │ ├── signer.ts │ ├── types │ ├── index.ts │ ├── sign.ts │ └── wallet.ts │ ├── utility │ ├── index.ts │ └── utility.ts │ ├── wallet.test.ts │ └── wallet.ts ├── tsconfig.json └── yarn.lock /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # CODEOWNERS: https://help.github.com/articles/about-codeowners/ 2 | 3 | # Primary repo maintainers 4 | * @zivkovicmilos 5 | 6 | # Special files 7 | LICENSE.md @jaekwon @moul 8 | .github/CODEOWNERS @jaekwon @moul 9 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # GitHub actions updates 4 | - package-ecosystem: 'github-actions' 5 | directory: '/' 6 | schedule: 7 | interval: 'daily' 8 | groups: 9 | actions: 10 | patterns: 11 | - '*' 12 | 13 | # Dependency updates 14 | - package-ecosystem: npm 15 | directory: / 16 | target-branch: 'main' 17 | schedule: 18 | interval: weekly 19 | ignore: 20 | # ignore all patch upgrades 21 | - dependency-name: '*' 22 | update-types: ['version-update:semver-patch'] 23 | open-pull-requests-limit: 10 24 | versioning-strategy: increase 25 | pull-request-branch-name: 26 | separator: '-' 27 | groups: 28 | eslint: 29 | patterns: 30 | - 'eslint' 31 | - 'eslint-config-prettier' 32 | - '@typescript-eslint/*' 33 | types: 34 | patterns: 35 | - '@types/*' 36 | prettier: 37 | patterns: 38 | - 'prettier' 39 | everything-else: 40 | patterns: 41 | - '*' 42 | reviewers: 43 | - 'zivkovicmilos' 44 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v4 11 | 12 | - name: Cache dependencies 13 | uses: actions/cache@v4 14 | with: 15 | path: ~/.yarn 16 | key: yarn-${{ hashFiles('yarn.lock') }} 17 | restore-keys: yarn- 18 | 19 | - name: Install modules 20 | run: yarn install 21 | 22 | - name: ESLint 23 | run: ./node_modules/.bin/eslint '**/*.ts' --fix 24 | 25 | - name: Prettier 26 | run: ./node_modules/.bin/prettier --check . 27 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | jobs: 8 | lint: 9 | name: Linter 10 | uses: ./.github/workflows/lint.yaml 11 | 12 | test: 13 | name: Tests 14 | uses: ./.github/workflows/test.yaml 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v4 11 | 12 | - name: Cache dependencies 13 | uses: actions/cache@v4 14 | with: 15 | path: ~/.yarn 16 | key: yarn-${{ hashFiles('yarn.lock') }} 17 | restore-keys: yarn- 18 | 19 | - name: Install modules 20 | run: yarn install 21 | 22 | - name: Tests 23 | run: ./node_modules/.bin/jest 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Build files 5 | bin 6 | 7 | # Dev environment metadata 8 | .idea 9 | .DS_Store 10 | *.log -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bin 3 | yarn.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "bracketSpacing": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 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 [yyyy] [name of copyright owner] 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 |

⚛️ Tendermint2 JS/TS Client ⚛️

2 | 3 | ## Overview 4 | 5 | `@gnolang/tm2-js-client` is a JavaScript/TypeScript client implementation for Tendermint2-based chains. It is designed 6 | to make it 7 | easy for developers to interact with TM2 chains, providing a simplified API for account and transaction management. By 8 | doing all the heavy lifting behind the scenes, `@gnolang/tm2-js-client` enables developers to focus on what really 9 | matters - 10 | building their dApps. 11 | 12 | ## Key Features 13 | 14 | - JSON-RPC and WebSocket client support via a `Provider` 15 | - Simple account and transaction management API with a `Wallet` 16 | - Designed for easy extension for custom TM2 chains, such as [Gnoland](https://gno.land) 17 | 18 | ## Installation 19 | 20 | To install `@gnolang/tm2-js-client`, use your preferred package manager: 21 | 22 | ```bash 23 | yarn add @gnolang/tm2-js-client 24 | ``` 25 | 26 | ```bash 27 | npm install @gnolang/tm2-js-client 28 | ``` 29 | 30 | ## Common Terminology 31 | 32 | ### Provider 33 | 34 | A `Provider` is an interface that abstracts the interaction with the Tendermint2 chain, making it easier for users to 35 | communicate with it. Rather than requiring users to understand which endpoints are exposed, what their return types are, 36 | and how they are parsed, the `Provider` abstraction handles all of this behind the scenes. It exposes useful API methods 37 | that users can use and expects concrete types in return. 38 | 39 | Currently, the `@gnolang/tm2-js-client` package provides support for two Provider implementations: 40 | 41 | - `JSON-RPC Provider`: executes each call as a separate HTTP RPC call. 42 | - `WS Provider`: executes each call through an active WebSocket connection, which requires closing when not needed 43 | anymore. 44 | 45 | ### Signer 46 | 47 | A `Signer` is an interface that abstracts the interaction with a single Secp256k1 key pair. It exposes methods for 48 | signing data, verifying signatures, and getting metadata associated with the key pair, such as the address. 49 | 50 | Currently, the `@gnolang/tm2-js-client` package provides support for two `Signer` implementations: 51 | 52 | - `Key`: a signer that is based on a raw Secp256k1 key pair. 53 | - `Ledger`: a signer that is based on a Ledger device, with all interaction flowing through the user's device. 54 | 55 | ### Wallet 56 | 57 | A `Wallet` is a user-facing API that is used to interact with an account. A `Wallet` instance is tied to a single key 58 | pair and essentially wraps the given `Provider` for that specific account. 59 | 60 | A wallet can be generated from a randomly generated seed, a private key, or instantiated using a Ledger device. 61 | 62 | Using the `Wallet`, users can easily interact with the Tendermint2 chain using their account without having to worry 63 | about account management. 64 | 65 | ## Documentation 66 | 67 | For the sake of keeping the README short and sweet, you can find the documentation and usage examples 68 | for the package [here](https://docs.gno.land/reference/tm2-js-client/). 69 | 70 | ## Acknowledgements 71 | 72 | `@gnolang/tm2-js-client` is, and will continue to be, [licensed under Apache 2](LICENSE). 73 | 74 | It is made by the community, for the community, and any contribution is greatly appreciated. 75 | 76 | A special thank-you goes out to the [Onbloc](https://github.com/onbloc) team, building 77 | [Adena wallet](https://github.com/onbloc/adena-wallet) and other gno projects, whose extended supported 78 | made this package possible. 79 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import pluginJs from '@eslint/js'; 3 | import tsEslint from 'typescript-eslint'; 4 | import tsParser from '@typescript-eslint/parser'; 5 | import eslintConfigPrettier from 'eslint-config-prettier'; 6 | 7 | export default [ 8 | eslintConfigPrettier, 9 | pluginJs.configs.recommended, 10 | ...tsEslint.configs.recommended, 11 | { 12 | ignores: ['bin/**/*'], 13 | }, 14 | { 15 | languageOptions: { globals: globals.browser, parser: tsParser }, 16 | rules: { 17 | '@typescript-eslint/no-explicit-any': 'warn', 18 | '@typescript-eslint/no-unused-vars': 'warn', 19 | 'no-async-promise-executor': 'warn', 20 | }, 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testEnvironment": "node", 4 | "modulePathIgnorePatterns": ["bin", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gnolang/tm2-js-client", 3 | "version": "1.2.4", 4 | "description": "Tendermint2 JS / TS Client", 5 | "main": "./bin/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/gnolang/tm2-js-client.git" 9 | }, 10 | "keywords": [ 11 | "tm2", 12 | "tendermint2", 13 | "sdk", 14 | "client", 15 | "js" 16 | ], 17 | "author": "Milos Zivkovic ", 18 | "license": "Apache-2.0", 19 | "homepage": "https://gno.land/", 20 | "files": [ 21 | "bin/**/*" 22 | ], 23 | "private": false, 24 | "publishConfig": { 25 | "access": "public", 26 | "registry": "https://registry.npmjs.org/" 27 | }, 28 | "devDependencies": { 29 | "@eslint/js": "^9.19.0", 30 | "@types/jest": "^29.5.12", 31 | "@types/long": "^5.0.0", 32 | "@types/node": "^22.12.0", 33 | "@types/ws": "^8.5.11", 34 | "@typescript-eslint/eslint-plugin": "^8.22.0", 35 | "@typescript-eslint/parser": "^8.22.0", 36 | "@typescript-eslint/typescript-estree": "^8.6.0", 37 | "eslint": "^9.19.0", 38 | "eslint-config-prettier": "^10.0.1", 39 | "eslint-plugin-prettier": "5.2.1", 40 | "globals": "^15.14.0", 41 | "jest": "^29.7.0", 42 | "jest-mock-extended": "^3.0.4", 43 | "jest-websocket-mock": "^2.4.0", 44 | "prettier": "^3.4.2", 45 | "ts-jest": "^29.2.2", 46 | "ts-proto": "^2.6.1", 47 | "typescript": "^5.7.3", 48 | "typescript-eslint": "^8.22.0" 49 | }, 50 | "dependencies": { 51 | "@cosmjs/amino": "^0.33.0", 52 | "@cosmjs/crypto": "^0.33.0", 53 | "@cosmjs/ledger-amino": "^0.33.0", 54 | "@types/uuid": "^10.0.0", 55 | "axios": "^1.7.2", 56 | "long": "^5.2.3", 57 | "protobufjs": "^7.4.0", 58 | "uuid": "^11.0.5", 59 | "ws": "^8.18.0" 60 | }, 61 | "scripts": { 62 | "tsc": "tsc", 63 | "lint": "eslint '**/*.ts' --fix", 64 | "prettier": "prettier --write .", 65 | "build": "yarn tsc", 66 | "test": "jest", 67 | "prepare": "yarn build", 68 | "prepublishOnly": "yarn lint && yarn prettier" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /proto/tm2/abci.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/any.proto"; 4 | 5 | package tm2.abci; 6 | 7 | message ResponseDeliverTx { 8 | ResponseBase response_base = 1 [json_name = "ResponseBase"]; 9 | sint64 gas_wanted = 2 [json_name = "GasWanted"]; 10 | sint64 gas_used = 3 [json_name = "GasUsed"]; 11 | } 12 | 13 | message ResponseBase { 14 | google.protobuf.Any error = 1 [json_name = "Error"]; 15 | bytes data = 2 [json_name = "Data"]; 16 | repeated google.protobuf.Any events = 3 [json_name = "Events"]; 17 | string log = 4 [json_name = "Log"]; 18 | string info = 5 [json_name = "Info"]; 19 | } -------------------------------------------------------------------------------- /proto/tm2/tx.proto: -------------------------------------------------------------------------------- 1 | syntax = 'proto3'; 2 | 3 | import "google/protobuf/any.proto"; 4 | 5 | package tm2.tx; 6 | 7 | message Tx { 8 | // specific message types 9 | repeated google.protobuf.Any messages = 1; 10 | // transaction costs (fee) 11 | TxFee fee = 2; 12 | // the signatures for the transaction 13 | repeated TxSignature signatures = 3; 14 | // memo attached to the transaction 15 | string memo = 4; 16 | } 17 | 18 | message TxFee { 19 | // gas limit 20 | sint64 gas_wanted = 1; 21 | // gas fee details () 22 | string gas_fee = 2; 23 | } 24 | 25 | message TxSignature { 26 | // public key associated with the signature 27 | google.protobuf.Any pub_key = 1; 28 | // the signature 29 | bytes signature = 2; 30 | } 31 | 32 | message PubKeySecp256k1 { 33 | bytes key = 1; 34 | } 35 | -------------------------------------------------------------------------------- /scripts/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROTO_PATH=./proto 4 | OUT_DIR=./src/proto 5 | 6 | FILES=$(find proto -type f -name "*.proto") 7 | 8 | mkdir -p ${OUT_DIR} 9 | 10 | for x in ${FILES}; do 11 | protoc \ 12 | --plugin="./node_modules/.bin/protoc-gen-ts_proto" \ 13 | --ts_proto_out="${OUT_DIR}" \ 14 | --proto_path="${PROTO_PATH}" \ 15 | --ts_proto_opt="esModuleInterop=true,forceLong=long,useOptionals=messages,useDate=false,snakeToCamel=true,emitDefaultValues=json-methods" \ 16 | ${x} 17 | done 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './provider'; 2 | export * from './wallet'; 3 | export * from './proto'; 4 | export * from './services'; 5 | -------------------------------------------------------------------------------- /src/proto/google/protobuf/any.ts: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-ts_proto. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-ts_proto v2.2.0 4 | // protoc v5.29.0 5 | // source: google/protobuf/any.proto 6 | 7 | /* eslint-disable */ 8 | import { BinaryReader, BinaryWriter } from '@bufbuild/protobuf/wire'; 9 | import Long from 'long'; 10 | 11 | export const protobufPackage = 'google.protobuf'; 12 | 13 | /** 14 | * `Any` contains an arbitrary serialized protocol buffer message along with a 15 | * URL that describes the type of the serialized message. 16 | * 17 | * Protobuf library provides support to pack/unpack Any values in the form 18 | * of utility functions or additional generated methods of the Any type. 19 | * 20 | * Example 1: Pack and unpack a message in C++. 21 | * 22 | * Foo foo = ...; 23 | * Any any; 24 | * any.PackFrom(foo); 25 | * ... 26 | * if (any.UnpackTo(&foo)) { 27 | * ... 28 | * } 29 | * 30 | * Example 2: Pack and unpack a message in Java. 31 | * 32 | * Foo foo = ...; 33 | * Any any = Any.pack(foo); 34 | * ... 35 | * if (any.is(Foo.class)) { 36 | * foo = any.unpack(Foo.class); 37 | * } 38 | * // or ... 39 | * if (any.isSameTypeAs(Foo.getDefaultInstance())) { 40 | * foo = any.unpack(Foo.getDefaultInstance()); 41 | * } 42 | * 43 | * Example 3: Pack and unpack a message in Python. 44 | * 45 | * foo = Foo(...) 46 | * any = Any() 47 | * any.Pack(foo) 48 | * ... 49 | * if any.Is(Foo.DESCRIPTOR): 50 | * any.Unpack(foo) 51 | * ... 52 | * 53 | * Example 4: Pack and unpack a message in Go 54 | * 55 | * foo := &pb.Foo{...} 56 | * any, err := anypb.New(foo) 57 | * if err != nil { 58 | * ... 59 | * } 60 | * ... 61 | * foo := &pb.Foo{} 62 | * if err := any.UnmarshalTo(foo); err != nil { 63 | * ... 64 | * } 65 | * 66 | * The pack methods provided by protobuf library will by default use 67 | * 'type.googleapis.com/full.type.name' as the type URL and the unpack 68 | * methods only use the fully qualified type name after the last '/' 69 | * in the type URL, for example "foo.bar.com/x/y.z" will yield type 70 | * name "y.z". 71 | * 72 | * JSON 73 | * ==== 74 | * The JSON representation of an `Any` value uses the regular 75 | * representation of the deserialized, embedded message, with an 76 | * additional field `@type` which contains the type URL. Example: 77 | * 78 | * package google.profile; 79 | * message Person { 80 | * string first_name = 1; 81 | * string last_name = 2; 82 | * } 83 | * 84 | * { 85 | * "@type": "type.googleapis.com/google.profile.Person", 86 | * "firstName": , 87 | * "lastName": 88 | * } 89 | * 90 | * If the embedded message type is well-known and has a custom JSON 91 | * representation, that representation will be embedded adding a field 92 | * `value` which holds the custom JSON in addition to the `@type` 93 | * field. Example (for message [google.protobuf.Duration][]): 94 | * 95 | * { 96 | * "@type": "type.googleapis.com/google.protobuf.Duration", 97 | * "value": "1.212s" 98 | * } 99 | */ 100 | export interface Any { 101 | /** 102 | * A URL/resource name that uniquely identifies the type of the serialized 103 | * protocol buffer message. This string must contain at least 104 | * one "/" character. The last segment of the URL's path must represent 105 | * the fully qualified name of the type (as in 106 | * `path/google.protobuf.Duration`). The name should be in a canonical form 107 | * (e.g., leading "." is not accepted). 108 | * 109 | * In practice, teams usually precompile into the binary all types that they 110 | * expect it to use in the context of Any. However, for URLs which use the 111 | * scheme `http`, `https`, or no scheme, one can optionally set up a type 112 | * server that maps type URLs to message definitions as follows: 113 | * 114 | * * If no scheme is provided, `https` is assumed. 115 | * * An HTTP GET on the URL must yield a [google.protobuf.Type][] 116 | * value in binary format, or produce an error. 117 | * * Applications are allowed to cache lookup results based on the 118 | * URL, or have them precompiled into a binary to avoid any 119 | * lookup. Therefore, binary compatibility needs to be preserved 120 | * on changes to types. (Use versioned type names to manage 121 | * breaking changes.) 122 | * 123 | * Note: this functionality is not currently available in the official 124 | * protobuf release, and it is not used for type URLs beginning with 125 | * type.googleapis.com. As of May 2023, there are no widely used type server 126 | * implementations and no plans to implement one. 127 | * 128 | * Schemes other than `http`, `https` (or the empty scheme) might be 129 | * used with implementation specific semantics. 130 | */ 131 | typeUrl: string; 132 | /** Must be a valid serialized protocol buffer of the above specified type. */ 133 | value: Uint8Array; 134 | } 135 | 136 | function createBaseAny(): Any { 137 | return { typeUrl: '', value: new Uint8Array(0) }; 138 | } 139 | 140 | export const Any: MessageFns = { 141 | encode( 142 | message: Any, 143 | writer: BinaryWriter = new BinaryWriter() 144 | ): BinaryWriter { 145 | if (message.typeUrl !== '') { 146 | writer.uint32(10).string(message.typeUrl); 147 | } 148 | if (message.value.length !== 0) { 149 | writer.uint32(18).bytes(message.value); 150 | } 151 | return writer; 152 | }, 153 | 154 | decode(input: BinaryReader | Uint8Array, length?: number): Any { 155 | const reader = 156 | input instanceof BinaryReader ? input : new BinaryReader(input); 157 | let end = length === undefined ? reader.len : reader.pos + length; 158 | const message = createBaseAny(); 159 | while (reader.pos < end) { 160 | const tag = reader.uint32(); 161 | switch (tag >>> 3) { 162 | case 1: 163 | if (tag !== 10) { 164 | break; 165 | } 166 | 167 | message.typeUrl = reader.string(); 168 | continue; 169 | case 2: 170 | if (tag !== 18) { 171 | break; 172 | } 173 | 174 | message.value = reader.bytes(); 175 | continue; 176 | } 177 | if ((tag & 7) === 4 || tag === 0) { 178 | break; 179 | } 180 | reader.skip(tag & 7); 181 | } 182 | return message; 183 | }, 184 | 185 | fromJSON(object: any): Any { 186 | return { 187 | typeUrl: isSet(object.typeUrl) ? globalThis.String(object.typeUrl) : '', 188 | value: isSet(object.value) 189 | ? bytesFromBase64(object.value) 190 | : new Uint8Array(0), 191 | }; 192 | }, 193 | 194 | toJSON(message: Any): unknown { 195 | const obj: any = {}; 196 | if (message.typeUrl !== undefined) { 197 | obj.typeUrl = message.typeUrl; 198 | } 199 | if (message.value !== undefined) { 200 | obj.value = base64FromBytes(message.value); 201 | } 202 | return obj; 203 | }, 204 | 205 | create, I>>(base?: I): Any { 206 | return Any.fromPartial(base ?? ({} as any)); 207 | }, 208 | fromPartial, I>>(object: I): Any { 209 | const message = createBaseAny(); 210 | message.typeUrl = object.typeUrl ?? ''; 211 | message.value = object.value ?? new Uint8Array(0); 212 | return message; 213 | }, 214 | }; 215 | 216 | function bytesFromBase64(b64: string): Uint8Array { 217 | if ((globalThis as any).Buffer) { 218 | return Uint8Array.from(globalThis.Buffer.from(b64, 'base64')); 219 | } else { 220 | const bin = globalThis.atob(b64); 221 | const arr = new Uint8Array(bin.length); 222 | for (let i = 0; i < bin.length; ++i) { 223 | arr[i] = bin.charCodeAt(i); 224 | } 225 | return arr; 226 | } 227 | } 228 | 229 | function base64FromBytes(arr: Uint8Array): string { 230 | if ((globalThis as any).Buffer) { 231 | return globalThis.Buffer.from(arr).toString('base64'); 232 | } else { 233 | const bin: string[] = []; 234 | arr.forEach((byte) => { 235 | bin.push(globalThis.String.fromCharCode(byte)); 236 | }); 237 | return globalThis.btoa(bin.join('')); 238 | } 239 | } 240 | 241 | type Builtin = 242 | | Date 243 | | Function 244 | | Uint8Array 245 | | string 246 | | number 247 | | boolean 248 | | undefined; 249 | 250 | export type DeepPartial = T extends Builtin 251 | ? T 252 | : T extends Long 253 | ? string | number | Long 254 | : T extends globalThis.Array 255 | ? globalThis.Array> 256 | : T extends ReadonlyArray 257 | ? ReadonlyArray> 258 | : T extends {} 259 | ? { [K in keyof T]?: DeepPartial } 260 | : Partial; 261 | 262 | type KeysOfUnion = T extends T ? keyof T : never; 263 | export type Exact = P extends Builtin 264 | ? P 265 | : P & { [K in keyof P]: Exact } & { 266 | [K in Exclude>]: never; 267 | }; 268 | 269 | function isSet(value: any): boolean { 270 | return value !== null && value !== undefined; 271 | } 272 | 273 | export interface MessageFns { 274 | encode(message: T, writer?: BinaryWriter): BinaryWriter; 275 | decode(input: BinaryReader | Uint8Array, length?: number): T; 276 | fromJSON(object: any): T; 277 | toJSON(message: T): unknown; 278 | create, I>>(base?: I): T; 279 | fromPartial, I>>(object: I): T; 280 | } 281 | -------------------------------------------------------------------------------- /src/proto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tm2/tx'; 2 | export { Any } from './google/protobuf/any'; 3 | -------------------------------------------------------------------------------- /src/proto/tm2/abci.ts: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-ts_proto. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-ts_proto v2.2.0 4 | // protoc v5.29.0 5 | // source: tm2/abci.proto 6 | 7 | /* eslint-disable */ 8 | import { BinaryReader, BinaryWriter } from '@bufbuild/protobuf/wire'; 9 | import Long from 'long'; 10 | import { Any } from '../google/protobuf/any'; 11 | 12 | export const protobufPackage = 'tm2.abci'; 13 | 14 | export interface ResponseDeliverTx { 15 | responseBase?: ResponseBase | undefined; 16 | gasWanted: Long; 17 | gasUsed: Long; 18 | } 19 | 20 | export interface ResponseBase { 21 | error?: Any | undefined; 22 | data: Uint8Array; 23 | events: Any[]; 24 | log: string; 25 | info: string; 26 | } 27 | 28 | function createBaseResponseDeliverTx(): ResponseDeliverTx { 29 | return { responseBase: undefined, gasWanted: Long.ZERO, gasUsed: Long.ZERO }; 30 | } 31 | 32 | export const ResponseDeliverTx: MessageFns = { 33 | encode( 34 | message: ResponseDeliverTx, 35 | writer: BinaryWriter = new BinaryWriter() 36 | ): BinaryWriter { 37 | if (message.responseBase !== undefined) { 38 | ResponseBase.encode( 39 | message.responseBase, 40 | writer.uint32(10).fork() 41 | ).join(); 42 | } 43 | if (!message.gasWanted.equals(Long.ZERO)) { 44 | writer.uint32(16).sint64(message.gasWanted.toString()); 45 | } 46 | if (!message.gasUsed.equals(Long.ZERO)) { 47 | writer.uint32(24).sint64(message.gasUsed.toString()); 48 | } 49 | return writer; 50 | }, 51 | 52 | decode(input: BinaryReader | Uint8Array, length?: number): ResponseDeliverTx { 53 | const reader = 54 | input instanceof BinaryReader ? input : new BinaryReader(input); 55 | let end = length === undefined ? reader.len : reader.pos + length; 56 | const message = createBaseResponseDeliverTx(); 57 | while (reader.pos < end) { 58 | const tag = reader.uint32(); 59 | switch (tag >>> 3) { 60 | case 1: 61 | if (tag !== 10) { 62 | break; 63 | } 64 | 65 | message.responseBase = ResponseBase.decode(reader, reader.uint32()); 66 | continue; 67 | case 2: 68 | if (tag !== 16) { 69 | break; 70 | } 71 | 72 | message.gasWanted = Long.fromString(reader.sint64().toString()); 73 | continue; 74 | case 3: 75 | if (tag !== 24) { 76 | break; 77 | } 78 | 79 | message.gasUsed = Long.fromString(reader.sint64().toString()); 80 | continue; 81 | } 82 | if ((tag & 7) === 4 || tag === 0) { 83 | break; 84 | } 85 | reader.skip(tag & 7); 86 | } 87 | return message; 88 | }, 89 | 90 | fromJSON(object: any): ResponseDeliverTx { 91 | return { 92 | responseBase: isSet(object.ResponseBase) 93 | ? ResponseBase.fromJSON(object.ResponseBase) 94 | : undefined, 95 | gasWanted: isSet(object.GasWanted) 96 | ? Long.fromValue(object.GasWanted) 97 | : Long.ZERO, 98 | gasUsed: isSet(object.GasUsed) 99 | ? Long.fromValue(object.GasUsed) 100 | : Long.ZERO, 101 | }; 102 | }, 103 | 104 | toJSON(message: ResponseDeliverTx): unknown { 105 | const obj: any = {}; 106 | if (message.responseBase !== undefined) { 107 | obj.ResponseBase = ResponseBase.toJSON(message.responseBase); 108 | } 109 | if (message.gasWanted !== undefined) { 110 | obj.GasWanted = (message.gasWanted || Long.ZERO).toString(); 111 | } 112 | if (message.gasUsed !== undefined) { 113 | obj.GasUsed = (message.gasUsed || Long.ZERO).toString(); 114 | } 115 | return obj; 116 | }, 117 | 118 | create, I>>( 119 | base?: I 120 | ): ResponseDeliverTx { 121 | return ResponseDeliverTx.fromPartial(base ?? ({} as any)); 122 | }, 123 | fromPartial, I>>( 124 | object: I 125 | ): ResponseDeliverTx { 126 | const message = createBaseResponseDeliverTx(); 127 | message.responseBase = 128 | object.responseBase !== undefined && object.responseBase !== null 129 | ? ResponseBase.fromPartial(object.responseBase) 130 | : undefined; 131 | message.gasWanted = 132 | object.gasWanted !== undefined && object.gasWanted !== null 133 | ? Long.fromValue(object.gasWanted) 134 | : Long.ZERO; 135 | message.gasUsed = 136 | object.gasUsed !== undefined && object.gasUsed !== null 137 | ? Long.fromValue(object.gasUsed) 138 | : Long.ZERO; 139 | return message; 140 | }, 141 | }; 142 | 143 | function createBaseResponseBase(): ResponseBase { 144 | return { 145 | error: undefined, 146 | data: new Uint8Array(0), 147 | events: [], 148 | log: '', 149 | info: '', 150 | }; 151 | } 152 | 153 | export const ResponseBase: MessageFns = { 154 | encode( 155 | message: ResponseBase, 156 | writer: BinaryWriter = new BinaryWriter() 157 | ): BinaryWriter { 158 | if (message.error !== undefined) { 159 | Any.encode(message.error, writer.uint32(10).fork()).join(); 160 | } 161 | if (message.data.length !== 0) { 162 | writer.uint32(18).bytes(message.data); 163 | } 164 | for (const v of message.events) { 165 | Any.encode(v!, writer.uint32(26).fork()).join(); 166 | } 167 | if (message.log !== '') { 168 | writer.uint32(34).string(message.log); 169 | } 170 | if (message.info !== '') { 171 | writer.uint32(42).string(message.info); 172 | } 173 | return writer; 174 | }, 175 | 176 | decode(input: BinaryReader | Uint8Array, length?: number): ResponseBase { 177 | const reader = 178 | input instanceof BinaryReader ? input : new BinaryReader(input); 179 | let end = length === undefined ? reader.len : reader.pos + length; 180 | const message = createBaseResponseBase(); 181 | while (reader.pos < end) { 182 | const tag = reader.uint32(); 183 | switch (tag >>> 3) { 184 | case 1: 185 | if (tag !== 10) { 186 | break; 187 | } 188 | 189 | message.error = Any.decode(reader, reader.uint32()); 190 | continue; 191 | case 2: 192 | if (tag !== 18) { 193 | break; 194 | } 195 | 196 | message.data = reader.bytes(); 197 | continue; 198 | case 3: 199 | if (tag !== 26) { 200 | break; 201 | } 202 | 203 | message.events.push(Any.decode(reader, reader.uint32())); 204 | continue; 205 | case 4: 206 | if (tag !== 34) { 207 | break; 208 | } 209 | 210 | message.log = reader.string(); 211 | continue; 212 | case 5: 213 | if (tag !== 42) { 214 | break; 215 | } 216 | 217 | message.info = reader.string(); 218 | continue; 219 | } 220 | if ((tag & 7) === 4 || tag === 0) { 221 | break; 222 | } 223 | reader.skip(tag & 7); 224 | } 225 | return message; 226 | }, 227 | 228 | fromJSON(object: any): ResponseBase { 229 | return { 230 | error: isSet(object.Error) ? Any.fromJSON(object.Error) : undefined, 231 | data: isSet(object.Data) 232 | ? bytesFromBase64(object.Data) 233 | : new Uint8Array(0), 234 | events: globalThis.Array.isArray(object?.Events) 235 | ? object.Events.map((e: any) => Any.fromJSON(e)) 236 | : [], 237 | log: isSet(object.Log) ? globalThis.String(object.Log) : '', 238 | info: isSet(object.Info) ? globalThis.String(object.Info) : '', 239 | }; 240 | }, 241 | 242 | toJSON(message: ResponseBase): unknown { 243 | const obj: any = {}; 244 | if (message.error !== undefined) { 245 | obj.Error = Any.toJSON(message.error); 246 | } 247 | if (message.data !== undefined) { 248 | obj.Data = base64FromBytes(message.data); 249 | } 250 | if (message.events?.length) { 251 | obj.Events = message.events.map((e) => Any.toJSON(e)); 252 | } 253 | if (message.log !== undefined) { 254 | obj.Log = message.log; 255 | } 256 | if (message.info !== undefined) { 257 | obj.Info = message.info; 258 | } 259 | return obj; 260 | }, 261 | 262 | create, I>>( 263 | base?: I 264 | ): ResponseBase { 265 | return ResponseBase.fromPartial(base ?? ({} as any)); 266 | }, 267 | fromPartial, I>>( 268 | object: I 269 | ): ResponseBase { 270 | const message = createBaseResponseBase(); 271 | message.error = 272 | object.error !== undefined && object.error !== null 273 | ? Any.fromPartial(object.error) 274 | : undefined; 275 | message.data = object.data ?? new Uint8Array(0); 276 | message.events = object.events?.map((e) => Any.fromPartial(e)) || []; 277 | message.log = object.log ?? ''; 278 | message.info = object.info ?? ''; 279 | return message; 280 | }, 281 | }; 282 | 283 | function bytesFromBase64(b64: string): Uint8Array { 284 | if ((globalThis as any).Buffer) { 285 | return Uint8Array.from(globalThis.Buffer.from(b64, 'base64')); 286 | } else { 287 | const bin = globalThis.atob(b64); 288 | const arr = new Uint8Array(bin.length); 289 | for (let i = 0; i < bin.length; ++i) { 290 | arr[i] = bin.charCodeAt(i); 291 | } 292 | return arr; 293 | } 294 | } 295 | 296 | function base64FromBytes(arr: Uint8Array): string { 297 | if ((globalThis as any).Buffer) { 298 | return globalThis.Buffer.from(arr).toString('base64'); 299 | } else { 300 | const bin: string[] = []; 301 | arr.forEach((byte) => { 302 | bin.push(globalThis.String.fromCharCode(byte)); 303 | }); 304 | return globalThis.btoa(bin.join('')); 305 | } 306 | } 307 | 308 | type Builtin = 309 | | Date 310 | | Function 311 | | Uint8Array 312 | | string 313 | | number 314 | | boolean 315 | | undefined; 316 | 317 | export type DeepPartial = T extends Builtin 318 | ? T 319 | : T extends Long 320 | ? string | number | Long 321 | : T extends globalThis.Array 322 | ? globalThis.Array> 323 | : T extends ReadonlyArray 324 | ? ReadonlyArray> 325 | : T extends {} 326 | ? { [K in keyof T]?: DeepPartial } 327 | : Partial; 328 | 329 | type KeysOfUnion = T extends T ? keyof T : never; 330 | export type Exact = P extends Builtin 331 | ? P 332 | : P & { [K in keyof P]: Exact } & { 333 | [K in Exclude>]: never; 334 | }; 335 | 336 | function isSet(value: any): boolean { 337 | return value !== null && value !== undefined; 338 | } 339 | 340 | export interface MessageFns { 341 | encode(message: T, writer?: BinaryWriter): BinaryWriter; 342 | decode(input: BinaryReader | Uint8Array, length?: number): T; 343 | fromJSON(object: any): T; 344 | toJSON(message: T): unknown; 345 | create, I>>(base?: I): T; 346 | fromPartial, I>>(object: I): T; 347 | } 348 | -------------------------------------------------------------------------------- /src/proto/tm2/tx.ts: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-ts_proto. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-ts_proto v2.2.0 4 | // protoc v5.29.0 5 | // source: tm2/tx.proto 6 | 7 | /* eslint-disable */ 8 | import { BinaryReader, BinaryWriter } from '@bufbuild/protobuf/wire'; 9 | import Long from 'long'; 10 | import { Any } from '../google/protobuf/any'; 11 | 12 | export const protobufPackage = 'tm2.tx'; 13 | 14 | export interface Tx { 15 | /** specific message types */ 16 | messages: Any[]; 17 | /** transaction costs (fee) */ 18 | fee?: TxFee | undefined; 19 | /** the signatures for the transaction */ 20 | signatures: TxSignature[]; 21 | /** memo attached to the transaction */ 22 | memo: string; 23 | } 24 | 25 | export interface TxFee { 26 | /** gas limit */ 27 | gasWanted: Long; 28 | /** gas fee details () */ 29 | gasFee: string; 30 | } 31 | 32 | export interface TxSignature { 33 | /** public key associated with the signature */ 34 | pubKey?: Any | undefined; 35 | /** the signature */ 36 | signature: Uint8Array; 37 | } 38 | 39 | export interface PubKeySecp256k1 { 40 | key: Uint8Array; 41 | } 42 | 43 | function createBaseTx(): Tx { 44 | return { messages: [], fee: undefined, signatures: [], memo: '' }; 45 | } 46 | 47 | export const Tx: MessageFns = { 48 | encode(message: Tx, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { 49 | for (const v of message.messages) { 50 | Any.encode(v!, writer.uint32(10).fork()).join(); 51 | } 52 | if (message.fee !== undefined) { 53 | TxFee.encode(message.fee, writer.uint32(18).fork()).join(); 54 | } 55 | for (const v of message.signatures) { 56 | TxSignature.encode(v!, writer.uint32(26).fork()).join(); 57 | } 58 | if (message.memo !== '') { 59 | writer.uint32(34).string(message.memo); 60 | } 61 | return writer; 62 | }, 63 | 64 | decode(input: BinaryReader | Uint8Array, length?: number): Tx { 65 | const reader = 66 | input instanceof BinaryReader ? input : new BinaryReader(input); 67 | let end = length === undefined ? reader.len : reader.pos + length; 68 | const message = createBaseTx(); 69 | while (reader.pos < end) { 70 | const tag = reader.uint32(); 71 | switch (tag >>> 3) { 72 | case 1: 73 | if (tag !== 10) { 74 | break; 75 | } 76 | 77 | message.messages.push(Any.decode(reader, reader.uint32())); 78 | continue; 79 | case 2: 80 | if (tag !== 18) { 81 | break; 82 | } 83 | 84 | message.fee = TxFee.decode(reader, reader.uint32()); 85 | continue; 86 | case 3: 87 | if (tag !== 26) { 88 | break; 89 | } 90 | 91 | message.signatures.push(TxSignature.decode(reader, reader.uint32())); 92 | continue; 93 | case 4: 94 | if (tag !== 34) { 95 | break; 96 | } 97 | 98 | message.memo = reader.string(); 99 | continue; 100 | } 101 | if ((tag & 7) === 4 || tag === 0) { 102 | break; 103 | } 104 | reader.skip(tag & 7); 105 | } 106 | return message; 107 | }, 108 | 109 | fromJSON(object: any): Tx { 110 | return { 111 | messages: globalThis.Array.isArray(object?.messages) 112 | ? object.messages.map((e: any) => Any.fromJSON(e)) 113 | : [], 114 | fee: isSet(object.fee) ? TxFee.fromJSON(object.fee) : undefined, 115 | signatures: globalThis.Array.isArray(object?.signatures) 116 | ? object.signatures.map((e: any) => TxSignature.fromJSON(e)) 117 | : [], 118 | memo: isSet(object.memo) ? globalThis.String(object.memo) : '', 119 | }; 120 | }, 121 | 122 | toJSON(message: Tx): unknown { 123 | const obj: any = {}; 124 | if (message.messages?.length) { 125 | obj.messages = message.messages.map((e) => Any.toJSON(e)); 126 | } 127 | if (message.fee !== undefined) { 128 | obj.fee = TxFee.toJSON(message.fee); 129 | } 130 | if (message.signatures?.length) { 131 | obj.signatures = message.signatures.map((e) => TxSignature.toJSON(e)); 132 | } 133 | if (message.memo !== undefined) { 134 | obj.memo = message.memo; 135 | } 136 | return obj; 137 | }, 138 | 139 | create, I>>(base?: I): Tx { 140 | return Tx.fromPartial(base ?? ({} as any)); 141 | }, 142 | fromPartial, I>>(object: I): Tx { 143 | const message = createBaseTx(); 144 | message.messages = object.messages?.map((e) => Any.fromPartial(e)) || []; 145 | message.fee = 146 | object.fee !== undefined && object.fee !== null 147 | ? TxFee.fromPartial(object.fee) 148 | : undefined; 149 | message.signatures = 150 | object.signatures?.map((e) => TxSignature.fromPartial(e)) || []; 151 | message.memo = object.memo ?? ''; 152 | return message; 153 | }, 154 | }; 155 | 156 | function createBaseTxFee(): TxFee { 157 | return { gasWanted: Long.ZERO, gasFee: '' }; 158 | } 159 | 160 | export const TxFee: MessageFns = { 161 | encode( 162 | message: TxFee, 163 | writer: BinaryWriter = new BinaryWriter() 164 | ): BinaryWriter { 165 | if (!message.gasWanted.equals(Long.ZERO)) { 166 | writer.uint32(8).sint64(message.gasWanted.toString()); 167 | } 168 | if (message.gasFee !== '') { 169 | writer.uint32(18).string(message.gasFee); 170 | } 171 | return writer; 172 | }, 173 | 174 | decode(input: BinaryReader | Uint8Array, length?: number): TxFee { 175 | const reader = 176 | input instanceof BinaryReader ? input : new BinaryReader(input); 177 | let end = length === undefined ? reader.len : reader.pos + length; 178 | const message = createBaseTxFee(); 179 | while (reader.pos < end) { 180 | const tag = reader.uint32(); 181 | switch (tag >>> 3) { 182 | case 1: 183 | if (tag !== 8) { 184 | break; 185 | } 186 | 187 | message.gasWanted = Long.fromString(reader.sint64().toString()); 188 | continue; 189 | case 2: 190 | if (tag !== 18) { 191 | break; 192 | } 193 | 194 | message.gasFee = reader.string(); 195 | continue; 196 | } 197 | if ((tag & 7) === 4 || tag === 0) { 198 | break; 199 | } 200 | reader.skip(tag & 7); 201 | } 202 | return message; 203 | }, 204 | 205 | fromJSON(object: any): TxFee { 206 | return { 207 | gasWanted: isSet(object.gasWanted) 208 | ? Long.fromValue(object.gasWanted) 209 | : Long.ZERO, 210 | gasFee: isSet(object.gasFee) ? globalThis.String(object.gasFee) : '', 211 | }; 212 | }, 213 | 214 | toJSON(message: TxFee): unknown { 215 | const obj: any = {}; 216 | if (message.gasWanted !== undefined) { 217 | obj.gasWanted = (message.gasWanted || Long.ZERO).toString(); 218 | } 219 | if (message.gasFee !== undefined) { 220 | obj.gasFee = message.gasFee; 221 | } 222 | return obj; 223 | }, 224 | 225 | create, I>>(base?: I): TxFee { 226 | return TxFee.fromPartial(base ?? ({} as any)); 227 | }, 228 | fromPartial, I>>(object: I): TxFee { 229 | const message = createBaseTxFee(); 230 | message.gasWanted = 231 | object.gasWanted !== undefined && object.gasWanted !== null 232 | ? Long.fromValue(object.gasWanted) 233 | : Long.ZERO; 234 | message.gasFee = object.gasFee ?? ''; 235 | return message; 236 | }, 237 | }; 238 | 239 | function createBaseTxSignature(): TxSignature { 240 | return { pubKey: undefined, signature: new Uint8Array(0) }; 241 | } 242 | 243 | export const TxSignature: MessageFns = { 244 | encode( 245 | message: TxSignature, 246 | writer: BinaryWriter = new BinaryWriter() 247 | ): BinaryWriter { 248 | if (message.pubKey !== undefined) { 249 | Any.encode(message.pubKey, writer.uint32(10).fork()).join(); 250 | } 251 | if (message.signature.length !== 0) { 252 | writer.uint32(18).bytes(message.signature); 253 | } 254 | return writer; 255 | }, 256 | 257 | decode(input: BinaryReader | Uint8Array, length?: number): TxSignature { 258 | const reader = 259 | input instanceof BinaryReader ? input : new BinaryReader(input); 260 | let end = length === undefined ? reader.len : reader.pos + length; 261 | const message = createBaseTxSignature(); 262 | while (reader.pos < end) { 263 | const tag = reader.uint32(); 264 | switch (tag >>> 3) { 265 | case 1: 266 | if (tag !== 10) { 267 | break; 268 | } 269 | 270 | message.pubKey = Any.decode(reader, reader.uint32()); 271 | continue; 272 | case 2: 273 | if (tag !== 18) { 274 | break; 275 | } 276 | 277 | message.signature = reader.bytes(); 278 | continue; 279 | } 280 | if ((tag & 7) === 4 || tag === 0) { 281 | break; 282 | } 283 | reader.skip(tag & 7); 284 | } 285 | return message; 286 | }, 287 | 288 | fromJSON(object: any): TxSignature { 289 | return { 290 | pubKey: isSet(object.pubKey) ? Any.fromJSON(object.pubKey) : undefined, 291 | signature: isSet(object.signature) 292 | ? bytesFromBase64(object.signature) 293 | : new Uint8Array(0), 294 | }; 295 | }, 296 | 297 | toJSON(message: TxSignature): unknown { 298 | const obj: any = {}; 299 | if (message.pubKey !== undefined) { 300 | obj.pubKey = Any.toJSON(message.pubKey); 301 | } 302 | if (message.signature !== undefined) { 303 | obj.signature = base64FromBytes(message.signature); 304 | } 305 | return obj; 306 | }, 307 | 308 | create, I>>(base?: I): TxSignature { 309 | return TxSignature.fromPartial(base ?? ({} as any)); 310 | }, 311 | fromPartial, I>>( 312 | object: I 313 | ): TxSignature { 314 | const message = createBaseTxSignature(); 315 | message.pubKey = 316 | object.pubKey !== undefined && object.pubKey !== null 317 | ? Any.fromPartial(object.pubKey) 318 | : undefined; 319 | message.signature = object.signature ?? new Uint8Array(0); 320 | return message; 321 | }, 322 | }; 323 | 324 | function createBasePubKeySecp256k1(): PubKeySecp256k1 { 325 | return { key: new Uint8Array(0) }; 326 | } 327 | 328 | export const PubKeySecp256k1: MessageFns = { 329 | encode( 330 | message: PubKeySecp256k1, 331 | writer: BinaryWriter = new BinaryWriter() 332 | ): BinaryWriter { 333 | if (message.key.length !== 0) { 334 | writer.uint32(10).bytes(message.key); 335 | } 336 | return writer; 337 | }, 338 | 339 | decode(input: BinaryReader | Uint8Array, length?: number): PubKeySecp256k1 { 340 | const reader = 341 | input instanceof BinaryReader ? input : new BinaryReader(input); 342 | let end = length === undefined ? reader.len : reader.pos + length; 343 | const message = createBasePubKeySecp256k1(); 344 | while (reader.pos < end) { 345 | const tag = reader.uint32(); 346 | switch (tag >>> 3) { 347 | case 1: 348 | if (tag !== 10) { 349 | break; 350 | } 351 | 352 | message.key = reader.bytes(); 353 | continue; 354 | } 355 | if ((tag & 7) === 4 || tag === 0) { 356 | break; 357 | } 358 | reader.skip(tag & 7); 359 | } 360 | return message; 361 | }, 362 | 363 | fromJSON(object: any): PubKeySecp256k1 { 364 | return { 365 | key: isSet(object.key) ? bytesFromBase64(object.key) : new Uint8Array(0), 366 | }; 367 | }, 368 | 369 | toJSON(message: PubKeySecp256k1): unknown { 370 | const obj: any = {}; 371 | if (message.key !== undefined) { 372 | obj.key = base64FromBytes(message.key); 373 | } 374 | return obj; 375 | }, 376 | 377 | create, I>>( 378 | base?: I 379 | ): PubKeySecp256k1 { 380 | return PubKeySecp256k1.fromPartial(base ?? ({} as any)); 381 | }, 382 | fromPartial, I>>( 383 | object: I 384 | ): PubKeySecp256k1 { 385 | const message = createBasePubKeySecp256k1(); 386 | message.key = object.key ?? new Uint8Array(0); 387 | return message; 388 | }, 389 | }; 390 | 391 | function bytesFromBase64(b64: string): Uint8Array { 392 | if ((globalThis as any).Buffer) { 393 | return Uint8Array.from(globalThis.Buffer.from(b64, 'base64')); 394 | } else { 395 | const bin = globalThis.atob(b64); 396 | const arr = new Uint8Array(bin.length); 397 | for (let i = 0; i < bin.length; ++i) { 398 | arr[i] = bin.charCodeAt(i); 399 | } 400 | return arr; 401 | } 402 | } 403 | 404 | function base64FromBytes(arr: Uint8Array): string { 405 | if ((globalThis as any).Buffer) { 406 | return globalThis.Buffer.from(arr).toString('base64'); 407 | } else { 408 | const bin: string[] = []; 409 | arr.forEach((byte) => { 410 | bin.push(globalThis.String.fromCharCode(byte)); 411 | }); 412 | return globalThis.btoa(bin.join('')); 413 | } 414 | } 415 | 416 | type Builtin = 417 | | Date 418 | | Function 419 | | Uint8Array 420 | | string 421 | | number 422 | | boolean 423 | | undefined; 424 | 425 | export type DeepPartial = T extends Builtin 426 | ? T 427 | : T extends Long 428 | ? string | number | Long 429 | : T extends globalThis.Array 430 | ? globalThis.Array> 431 | : T extends ReadonlyArray 432 | ? ReadonlyArray> 433 | : T extends {} 434 | ? { [K in keyof T]?: DeepPartial } 435 | : Partial; 436 | 437 | type KeysOfUnion = T extends T ? keyof T : never; 438 | export type Exact = P extends Builtin 439 | ? P 440 | : P & { [K in keyof P]: Exact } & { 441 | [K in Exclude>]: never; 442 | }; 443 | 444 | function isSet(value: any): boolean { 445 | return value !== null && value !== undefined; 446 | } 447 | 448 | export interface MessageFns { 449 | encode(message: T, writer?: BinaryWriter): BinaryWriter; 450 | decode(input: BinaryReader | Uint8Array, length?: number): T; 451 | fromJSON(object: any): T; 452 | toJSON(message: T): unknown; 453 | create, I>>(base?: I): T; 454 | fromPartial, I>>(object: I): T; 455 | } 456 | -------------------------------------------------------------------------------- /src/provider/endpoints.ts: -------------------------------------------------------------------------------- 1 | export enum CommonEndpoint { 2 | HEALTH = 'health', 3 | STATUS = 'status', 4 | } 5 | 6 | export enum ConsensusEndpoint { 7 | NET_INFO = 'net_info', 8 | GENESIS = 'genesis', 9 | CONSENSUS_PARAMS = 'consensus_params', 10 | CONSENSUS_STATE = 'consensus_state', 11 | COMMIT = 'commit', 12 | VALIDATORS = 'validators', 13 | } 14 | 15 | export enum BlockEndpoint { 16 | BLOCK = 'block', 17 | BLOCK_RESULTS = 'block_results', 18 | BLOCKCHAIN = 'blockchain', 19 | } 20 | 21 | export enum TransactionEndpoint { 22 | NUM_UNCONFIRMED_TXS = 'num_unconfirmed_txs', 23 | UNCONFIRMED_TXS = 'unconfirmed_txs', 24 | BROADCAST_TX_ASYNC = 'broadcast_tx_async', 25 | BROADCAST_TX_SYNC = 'broadcast_tx_sync', 26 | BROADCAST_TX_COMMIT = 'broadcast_tx_commit', 27 | TX = 'tx', 28 | } 29 | 30 | export enum ABCIEndpoint { 31 | ABCI_INFO = 'abci_info', 32 | ABCI_QUERY = 'abci_query', 33 | } 34 | -------------------------------------------------------------------------------- /src/provider/errors/errors.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GasOverflowErrorMessage, 3 | InsufficientCoinsErrorMessage, 4 | InsufficientFeeErrorMessage, 5 | InsufficientFundsErrorMessage, 6 | InternalErrorMessage, 7 | InvalidAddressErrorMessage, 8 | InvalidCoinsErrorMessage, 9 | InvalidGasWantedErrorMessage, 10 | InvalidPubKeyErrorMessage, 11 | InvalidSequenceErrorMessage, 12 | MemoTooLargeErrorMessage, 13 | NoSignaturesErrorMessage, 14 | OutOfGasErrorMessage, 15 | TooManySignaturesErrorMessage, 16 | TxDecodeErrorMessage, 17 | UnauthorizedErrorMessage, 18 | UnknownAddressErrorMessage, 19 | UnknownRequestErrorMessage, 20 | } from './messages'; 21 | 22 | class TM2Error extends Error { 23 | public log?: string; 24 | 25 | constructor(message: string, log?: string) { 26 | super(message); 27 | 28 | this.log = log; 29 | } 30 | } 31 | 32 | class InternalError extends TM2Error { 33 | constructor(log?: string) { 34 | super(InternalErrorMessage, log); 35 | } 36 | } 37 | 38 | class TxDecodeError extends TM2Error { 39 | constructor(log?: string) { 40 | super(TxDecodeErrorMessage, log); 41 | } 42 | } 43 | 44 | class InvalidSequenceError extends TM2Error { 45 | constructor(log?: string) { 46 | super(InvalidSequenceErrorMessage, log); 47 | } 48 | } 49 | 50 | class UnauthorizedError extends TM2Error { 51 | constructor(log?: string) { 52 | super(UnauthorizedErrorMessage, log); 53 | } 54 | } 55 | 56 | class InsufficientFundsError extends TM2Error { 57 | constructor(log?: string) { 58 | super(InsufficientFundsErrorMessage, log); 59 | } 60 | } 61 | 62 | class UnknownRequestError extends TM2Error { 63 | constructor(log?: string) { 64 | super(UnknownRequestErrorMessage, log); 65 | } 66 | } 67 | 68 | class InvalidAddressError extends TM2Error { 69 | constructor(log?: string) { 70 | super(InvalidAddressErrorMessage, log); 71 | } 72 | } 73 | 74 | class UnknownAddressError extends TM2Error { 75 | constructor(log?: string) { 76 | super(UnknownAddressErrorMessage, log); 77 | } 78 | } 79 | 80 | class InvalidPubKeyError extends TM2Error { 81 | constructor(log?: string) { 82 | super(InvalidPubKeyErrorMessage, log); 83 | } 84 | } 85 | 86 | class InsufficientCoinsError extends TM2Error { 87 | constructor(log?: string) { 88 | super(InsufficientCoinsErrorMessage, log); 89 | } 90 | } 91 | 92 | class InvalidCoinsError extends TM2Error { 93 | constructor(log?: string) { 94 | super(InvalidCoinsErrorMessage, log); 95 | } 96 | } 97 | 98 | class InvalidGasWantedError extends TM2Error { 99 | constructor(log?: string) { 100 | super(InvalidGasWantedErrorMessage, log); 101 | } 102 | } 103 | 104 | class OutOfGasError extends TM2Error { 105 | constructor(log?: string) { 106 | super(OutOfGasErrorMessage, log); 107 | } 108 | } 109 | 110 | class MemoTooLargeError extends TM2Error { 111 | constructor(log?: string) { 112 | super(MemoTooLargeErrorMessage, log); 113 | } 114 | } 115 | 116 | class InsufficientFeeError extends TM2Error { 117 | constructor(log?: string) { 118 | super(InsufficientFeeErrorMessage, log); 119 | } 120 | } 121 | 122 | class TooManySignaturesError extends TM2Error { 123 | constructor(log?: string) { 124 | super(TooManySignaturesErrorMessage, log); 125 | } 126 | } 127 | 128 | class NoSignaturesError extends TM2Error { 129 | constructor(log?: string) { 130 | super(NoSignaturesErrorMessage, log); 131 | } 132 | } 133 | 134 | class GasOverflowError extends TM2Error { 135 | constructor(log?: string) { 136 | super(GasOverflowErrorMessage, log); 137 | } 138 | } 139 | 140 | export { 141 | TM2Error, 142 | InternalError, 143 | TxDecodeError, 144 | InvalidSequenceError, 145 | UnauthorizedError, 146 | InsufficientFundsError, 147 | UnknownRequestError, 148 | InvalidAddressError, 149 | UnknownAddressError, 150 | InvalidPubKeyError, 151 | InsufficientCoinsError, 152 | InvalidCoinsError, 153 | InvalidGasWantedError, 154 | OutOfGasError, 155 | MemoTooLargeError, 156 | InsufficientFeeError, 157 | TooManySignaturesError, 158 | NoSignaturesError, 159 | GasOverflowError, 160 | }; 161 | -------------------------------------------------------------------------------- /src/provider/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors'; 2 | -------------------------------------------------------------------------------- /src/provider/errors/messages.ts: -------------------------------------------------------------------------------- 1 | // Errors constructed from: 2 | // https://github.com/gnolang/gno/blob/master/tm2/pkg/std/errors.go 3 | 4 | const InternalErrorMessage = 'internal error encountered'; 5 | const TxDecodeErrorMessage = 'unable to decode tx'; 6 | const InvalidSequenceErrorMessage = 'invalid sequence'; 7 | const UnauthorizedErrorMessage = 'signature is unauthorized'; 8 | const InsufficientFundsErrorMessage = 'insufficient funds'; 9 | const UnknownRequestErrorMessage = 'unknown request'; 10 | const InvalidAddressErrorMessage = 'invalid address'; 11 | const UnknownAddressErrorMessage = 'unknown address'; 12 | const InvalidPubKeyErrorMessage = 'invalid pubkey'; 13 | const InsufficientCoinsErrorMessage = 'insufficient coins'; 14 | const InvalidCoinsErrorMessage = 'invalid coins'; 15 | const InvalidGasWantedErrorMessage = 'invalid gas wanted'; 16 | const OutOfGasErrorMessage = 'out of gas'; 17 | const MemoTooLargeErrorMessage = 'memo too large'; 18 | const InsufficientFeeErrorMessage = 'insufficient fee'; 19 | const TooManySignaturesErrorMessage = 'too many signatures'; 20 | const NoSignaturesErrorMessage = 'no signatures'; 21 | const GasOverflowErrorMessage = 'gas overflow'; 22 | 23 | export { 24 | InternalErrorMessage, 25 | TxDecodeErrorMessage, 26 | InvalidSequenceErrorMessage, 27 | UnauthorizedErrorMessage, 28 | InsufficientFundsErrorMessage, 29 | UnknownRequestErrorMessage, 30 | InvalidAddressErrorMessage, 31 | UnknownAddressErrorMessage, 32 | InvalidPubKeyErrorMessage, 33 | InsufficientCoinsErrorMessage, 34 | InvalidCoinsErrorMessage, 35 | InvalidGasWantedErrorMessage, 36 | OutOfGasErrorMessage, 37 | MemoTooLargeErrorMessage, 38 | InsufficientFeeErrorMessage, 39 | TooManySignaturesErrorMessage, 40 | NoSignaturesErrorMessage, 41 | GasOverflowErrorMessage, 42 | }; 43 | -------------------------------------------------------------------------------- /src/provider/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jsonrpc'; 2 | export * from './types'; 3 | export * from './utility'; 4 | export * from './websocket'; 5 | export * from './endpoints'; 6 | export * from './provider'; 7 | export * from './errors'; 8 | -------------------------------------------------------------------------------- /src/provider/jsonrpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jsonrpc'; 2 | -------------------------------------------------------------------------------- /src/provider/jsonrpc/jsonrpc.test.ts: -------------------------------------------------------------------------------- 1 | import { sha256 } from '@cosmjs/crypto'; 2 | import axios from 'axios'; 3 | import { mock } from 'jest-mock-extended'; 4 | import Long from 'long'; 5 | import { Tx } from '../../proto'; 6 | import { CommonEndpoint, TransactionEndpoint } from '../endpoints'; 7 | import { TM2Error } from '../errors'; 8 | import { UnauthorizedErrorMessage } from '../errors/messages'; 9 | import { 10 | ABCIAccount, 11 | ABCIErrorKey, 12 | ABCIResponse, 13 | BlockInfo, 14 | BlockResult, 15 | BroadcastTxSyncResult, 16 | ConsensusParams, 17 | NetworkInfo, 18 | RPCRequest, 19 | Status, 20 | } from '../types'; 21 | import { newResponse, stringToBase64, uint8ArrayToBase64 } from '../utility'; 22 | import { JSONRPCProvider } from './jsonrpc'; 23 | 24 | jest.mock('axios'); 25 | 26 | const mockedAxios = axios as jest.Mocked; 27 | const mockURL = '127.0.0.1:26657'; 28 | 29 | describe('JSON-RPC Provider', () => { 30 | test('estimateGas', async () => { 31 | const tx = Tx.fromJSON({ 32 | signatures: [], 33 | fee: { 34 | gasFee: '', 35 | gasWanted: new Long(0), 36 | }, 37 | messages: [], 38 | memo: '', 39 | }); 40 | const expectedEstimation = 44900; 41 | 42 | const mockABCIResponse: ABCIResponse = mock(); 43 | mockABCIResponse.response.Value = 44 | 'CiMiIW1zZzowLHN1Y2Nlc3M6dHJ1ZSxsb2c6LGV2ZW50czpbXRCAiXoYyL0F'; 45 | 46 | mockedAxios.post.mockResolvedValue({ 47 | data: newResponse(mockABCIResponse), 48 | }); 49 | 50 | // Create the provider 51 | const provider = new JSONRPCProvider(mockURL); 52 | const estimation = await provider.estimateGas(tx); 53 | 54 | expect(axios.post).toHaveBeenCalled(); 55 | expect(estimation).toEqual(expectedEstimation); 56 | }); 57 | 58 | test('getNetwork', async () => { 59 | const mockInfo: NetworkInfo = mock(); 60 | mockInfo.listening = false; 61 | 62 | mockedAxios.post.mockResolvedValue({ 63 | data: newResponse(mockInfo), 64 | }); 65 | 66 | // Create the provider 67 | const provider = new JSONRPCProvider(mockURL); 68 | const info = await provider.getNetwork(); 69 | 70 | expect(axios.post).toHaveBeenCalled(); 71 | expect(info).toEqual(mockInfo); 72 | }); 73 | 74 | test('getBlock', async () => { 75 | const mockInfo: BlockInfo = mock(); 76 | 77 | mockedAxios.post.mockResolvedValue({ 78 | data: newResponse(mockInfo), 79 | }); 80 | 81 | // Create the provider 82 | const provider = new JSONRPCProvider(mockURL); 83 | const info = await provider.getBlock(0); 84 | 85 | expect(axios.post).toHaveBeenCalled(); 86 | expect(info).toEqual(mockInfo); 87 | }); 88 | 89 | test('getBlockResult', async () => { 90 | const mockResult: BlockResult = mock(); 91 | 92 | mockedAxios.post.mockResolvedValue({ 93 | data: newResponse(mockResult), 94 | }); 95 | 96 | // Create the provider 97 | const provider = new JSONRPCProvider(mockURL); 98 | const result = await provider.getBlockResult(0); 99 | 100 | expect(axios.post).toHaveBeenCalled(); 101 | expect(result).toEqual(mockResult); 102 | }); 103 | 104 | describe('sendTransaction', () => { 105 | const validResult: BroadcastTxSyncResult = { 106 | error: null, 107 | data: null, 108 | Log: '', 109 | hash: 'hash123', 110 | }; 111 | 112 | const mockError = '/std.UnauthorizedError'; 113 | const mockLog = 'random error message'; 114 | const invalidResult: BroadcastTxSyncResult = { 115 | error: { 116 | [ABCIErrorKey]: mockError, 117 | }, 118 | data: null, 119 | Log: mockLog, 120 | hash: '', 121 | }; 122 | 123 | test.each([ 124 | [validResult, validResult.hash, '', ''], // no error 125 | [invalidResult, invalidResult.hash, UnauthorizedErrorMessage, mockLog], // error out 126 | ])('case %#', async (response, expectedHash, expectedErr, expectedLog) => { 127 | mockedAxios.post.mockResolvedValue({ 128 | data: newResponse(response), 129 | }); 130 | 131 | try { 132 | // Create the provider 133 | const provider = new JSONRPCProvider(mockURL); 134 | const tx = await provider.sendTransaction( 135 | 'encoded tx', 136 | TransactionEndpoint.BROADCAST_TX_SYNC 137 | ); 138 | 139 | expect(axios.post).toHaveBeenCalled(); 140 | expect(tx.hash).toEqual(expectedHash); 141 | 142 | if (expectedErr != '') { 143 | fail('expected error'); 144 | } 145 | } catch (e) { 146 | expect((e as Error).message).toBe(expectedErr); 147 | expect((e as TM2Error).log).toBe(expectedLog); 148 | } 149 | }); 150 | }); 151 | 152 | test('waitForTransaction', async () => { 153 | const emptyBlock: BlockInfo = mock(); 154 | emptyBlock.block.data = { 155 | txs: [], 156 | }; 157 | 158 | const tx: Tx = { 159 | messages: [], 160 | signatures: [], 161 | memo: 'tx memo', 162 | }; 163 | 164 | const encodedTx = Tx.encode(tx).finish(); 165 | const txHash = sha256(encodedTx); 166 | 167 | const filledBlock: BlockInfo = mock(); 168 | filledBlock.block.data = { 169 | txs: [uint8ArrayToBase64(encodedTx)], 170 | }; 171 | 172 | const latestBlock = 5; 173 | const startBlock = latestBlock - 2; 174 | 175 | const mockStatus: Status = mock(); 176 | mockStatus.sync_info.latest_block_height = `${latestBlock}`; 177 | 178 | const responseMap: Map = new Map([ 179 | [latestBlock, filledBlock], 180 | [latestBlock - 1, emptyBlock], 181 | [startBlock, emptyBlock], 182 | ]); 183 | 184 | mockedAxios.post.mockImplementation((url, params, config): Promise => { 185 | const request = params as RPCRequest; 186 | 187 | if (request.method == CommonEndpoint.STATUS) { 188 | return Promise.resolve({ 189 | data: newResponse(mockStatus), 190 | }); 191 | } 192 | 193 | if (!request.params) { 194 | return Promise.reject('invalid params'); 195 | } 196 | 197 | const blockNum: number = +(request.params[0] as string[]); 198 | const info = responseMap.get(blockNum); 199 | 200 | return Promise.resolve({ 201 | data: newResponse(info), 202 | }); 203 | }); 204 | 205 | // Create the provider 206 | const provider = new JSONRPCProvider(mockURL); 207 | const receivedTx = await provider.waitForTransaction( 208 | uint8ArrayToBase64(txHash), 209 | startBlock 210 | ); 211 | 212 | expect(axios.post).toHaveBeenCalled(); 213 | expect(receivedTx).toEqual(tx); 214 | }); 215 | 216 | test('getConsensusParams', async () => { 217 | const mockParams: ConsensusParams = mock(); 218 | mockParams.block_height = '1'; 219 | 220 | mockedAxios.post.mockResolvedValue({ 221 | data: newResponse(mockParams), 222 | }); 223 | 224 | // Create the provider 225 | const provider = new JSONRPCProvider(mockURL); 226 | const params = await provider.getConsensusParams(1); 227 | 228 | expect(axios.post).toHaveBeenCalled(); 229 | expect(params).toEqual(mockParams); 230 | }); 231 | 232 | test('getStatus', async () => { 233 | const mockStatus: Status = mock(); 234 | mockStatus.validator_info.address = 'address'; 235 | 236 | mockedAxios.post.mockResolvedValue({ 237 | data: newResponse(mockStatus), 238 | }); 239 | 240 | // Create the provider 241 | const provider = new JSONRPCProvider(mockURL); 242 | const status = await provider.getStatus(); 243 | 244 | expect(axios.post).toHaveBeenCalled(); 245 | expect(status).toEqual(mockStatus); 246 | }); 247 | 248 | test('getBlockNumber', async () => { 249 | const expectedBlockNumber = 10; 250 | const mockStatus: Status = mock(); 251 | mockStatus.sync_info.latest_block_height = `${expectedBlockNumber}`; 252 | 253 | mockedAxios.post.mockResolvedValue({ 254 | data: newResponse(mockStatus), 255 | }); 256 | 257 | // Create the provider 258 | const provider = new JSONRPCProvider(mockURL); 259 | const blockNumber = await provider.getBlockNumber(); 260 | 261 | expect(axios.post).toHaveBeenCalled(); 262 | expect(blockNumber).toEqual(expectedBlockNumber); 263 | }); 264 | 265 | describe('getBalance', () => { 266 | const denomination = 'atom'; 267 | test.each([ 268 | ['"5gnot,100atom"', 100], // balance found 269 | ['"5universe"', 0], // balance not found 270 | ['""', 0], // account doesn't exist 271 | ])('case %#', async (existing, expected) => { 272 | const mockABCIResponse: ABCIResponse = mock(); 273 | mockABCIResponse.response.ResponseBase = { 274 | Log: '', 275 | Info: '', 276 | Data: stringToBase64(existing), 277 | Error: null, 278 | Events: null, 279 | }; 280 | 281 | mockedAxios.post.mockResolvedValue({ 282 | data: newResponse(mockABCIResponse), 283 | }); 284 | 285 | // Create the provider 286 | const provider = new JSONRPCProvider(mockURL); 287 | const balance = await provider.getBalance('address', denomination); 288 | 289 | expect(axios.post).toHaveBeenCalled(); 290 | expect(balance).toBe(expected); 291 | }); 292 | }); 293 | 294 | describe('getSequence', () => { 295 | const validAccount: ABCIAccount = { 296 | BaseAccount: { 297 | address: 'random address', 298 | coins: '', 299 | public_key: null, 300 | account_number: '0', 301 | sequence: '10', 302 | }, 303 | }; 304 | 305 | test.each([ 306 | [ 307 | JSON.stringify(validAccount), 308 | parseInt(validAccount.BaseAccount.sequence, 10), 309 | ], // account exists 310 | ['null', 0], // account doesn't exist 311 | ])('case %#', async (response, expected) => { 312 | const mockABCIResponse: ABCIResponse = mock(); 313 | mockABCIResponse.response.ResponseBase = { 314 | Log: '', 315 | Info: '', 316 | Data: stringToBase64(response), 317 | Error: null, 318 | Events: null, 319 | }; 320 | 321 | mockedAxios.post.mockResolvedValue({ 322 | data: newResponse(mockABCIResponse), 323 | }); 324 | 325 | // Create the provider 326 | const provider = new JSONRPCProvider(mockURL); 327 | const sequence = await provider.getAccountSequence('address'); 328 | 329 | expect(axios.post).toHaveBeenCalled(); 330 | expect(sequence).toBe(expected); 331 | }); 332 | }); 333 | 334 | describe('getAccountNumber', () => { 335 | const validAccount: ABCIAccount = { 336 | BaseAccount: { 337 | address: 'random address', 338 | coins: '', 339 | public_key: null, 340 | account_number: '10', 341 | sequence: '0', 342 | }, 343 | }; 344 | 345 | test.each([ 346 | [ 347 | JSON.stringify(validAccount), 348 | parseInt(validAccount.BaseAccount.account_number, 10), 349 | ], // account exists 350 | ['null', 0], // account doesn't exist 351 | ])('case %#', async (response, expected) => { 352 | const mockABCIResponse: ABCIResponse = mock(); 353 | mockABCIResponse.response.ResponseBase = { 354 | Log: '', 355 | Info: '', 356 | Data: stringToBase64(response), 357 | Error: null, 358 | Events: null, 359 | }; 360 | 361 | mockedAxios.post.mockResolvedValue({ 362 | data: newResponse(mockABCIResponse), 363 | }); 364 | 365 | try { 366 | // Create the provider 367 | const provider = new JSONRPCProvider(mockURL); 368 | const accountNumber = await provider.getAccountNumber('address'); 369 | 370 | expect(axios.post).toHaveBeenCalled(); 371 | expect(accountNumber).toBe(expected); 372 | } catch (e) { 373 | expect((e as Error).message).toContain('account is not initialized'); 374 | } 375 | }); 376 | }); 377 | }); 378 | -------------------------------------------------------------------------------- /src/provider/jsonrpc/jsonrpc.ts: -------------------------------------------------------------------------------- 1 | import { Tx } from '../../proto'; 2 | import { RestService } from '../../services'; 3 | import { 4 | ABCIEndpoint, 5 | BlockEndpoint, 6 | CommonEndpoint, 7 | ConsensusEndpoint, 8 | TransactionEndpoint, 9 | } from '../endpoints'; 10 | import { Provider } from '../provider'; 11 | import { 12 | ABCIErrorKey, 13 | ABCIResponse, 14 | BlockInfo, 15 | BlockResult, 16 | BroadcastTransactionMap, 17 | BroadcastTxCommitResult, 18 | BroadcastTxSyncResult, 19 | ConsensusParams, 20 | NetworkInfo, 21 | RPCRequest, 22 | Status, 23 | TxResult, 24 | } from '../types'; 25 | import { 26 | extractAccountNumberFromResponse, 27 | extractBalanceFromResponse, 28 | extractSequenceFromResponse, 29 | extractSimulateFromResponse, 30 | newRequest, 31 | uint8ArrayToBase64, 32 | waitForTransaction, 33 | } from '../utility'; 34 | import { constructRequestError } from '../utility/errors.utility'; 35 | 36 | /** 37 | * Provider based on JSON-RPC HTTP requests 38 | */ 39 | export class JSONRPCProvider implements Provider { 40 | protected readonly baseURL: string; 41 | 42 | /** 43 | * Creates a new instance of the JSON-RPC Provider 44 | * @param {string} baseURL the JSON-RPC URL of the node 45 | */ 46 | constructor(baseURL: string) { 47 | this.baseURL = baseURL; 48 | } 49 | 50 | async estimateGas(tx: Tx): Promise { 51 | const encodedTx = uint8ArrayToBase64(Tx.encode(tx).finish()); 52 | const abciResponse: ABCIResponse = await RestService.post( 53 | this.baseURL, 54 | { 55 | request: newRequest(ABCIEndpoint.ABCI_QUERY, [ 56 | `.app/simulate`, 57 | `${encodedTx}`, 58 | '0', // Height; not supported > 0 for now 59 | false, 60 | ]), 61 | } 62 | ); 63 | 64 | const simulateResult = extractSimulateFromResponse(abciResponse); 65 | 66 | return simulateResult.gasUsed.toInt(); 67 | } 68 | 69 | async getBalance( 70 | address: string, 71 | denomination?: string, 72 | height?: number 73 | ): Promise { 74 | const abciResponse: ABCIResponse = await RestService.post( 75 | this.baseURL, 76 | { 77 | request: newRequest(ABCIEndpoint.ABCI_QUERY, [ 78 | `bank/balances/${address}`, 79 | '', 80 | '0', // Height; not supported > 0 for now 81 | false, 82 | ]), 83 | } 84 | ); 85 | 86 | return extractBalanceFromResponse( 87 | abciResponse.response.ResponseBase.Data, 88 | denomination ? denomination : 'ugnot' 89 | ); 90 | } 91 | 92 | async getBlock(height: number): Promise { 93 | return await RestService.post(this.baseURL, { 94 | request: newRequest(BlockEndpoint.BLOCK, [height.toString()]), 95 | }); 96 | } 97 | 98 | async getBlockResult(height: number): Promise { 99 | return await RestService.post(this.baseURL, { 100 | request: newRequest(BlockEndpoint.BLOCK_RESULTS, [height.toString()]), 101 | }); 102 | } 103 | 104 | async getBlockNumber(): Promise { 105 | // Fetch the status for the latest info 106 | const status = await this.getStatus(); 107 | 108 | return parseInt(status.sync_info.latest_block_height); 109 | } 110 | 111 | async getConsensusParams(height: number): Promise { 112 | return await RestService.post(this.baseURL, { 113 | request: newRequest(ConsensusEndpoint.CONSENSUS_PARAMS, [ 114 | height.toString(), 115 | ]), 116 | }); 117 | } 118 | 119 | getGasPrice(): Promise { 120 | return Promise.reject('not supported'); 121 | } 122 | 123 | async getNetwork(): Promise { 124 | return await RestService.post(this.baseURL, { 125 | request: newRequest(ConsensusEndpoint.NET_INFO), 126 | }); 127 | } 128 | 129 | async getAccountSequence(address: string, height?: number): Promise { 130 | const abciResponse: ABCIResponse = await RestService.post( 131 | this.baseURL, 132 | { 133 | request: newRequest(ABCIEndpoint.ABCI_QUERY, [ 134 | `auth/accounts/${address}`, 135 | '', 136 | '0', // Height; not supported > 0 for now 137 | false, 138 | ]), 139 | } 140 | ); 141 | 142 | return extractSequenceFromResponse(abciResponse.response.ResponseBase.Data); 143 | } 144 | 145 | async getAccountNumber(address: string, height?: number): Promise { 146 | const abciResponse: ABCIResponse = await RestService.post( 147 | this.baseURL, 148 | { 149 | request: newRequest(ABCIEndpoint.ABCI_QUERY, [ 150 | `auth/accounts/${address}`, 151 | '', 152 | '0', // Height; not supported > 0 for now 153 | false, 154 | ]), 155 | } 156 | ); 157 | 158 | return extractAccountNumberFromResponse( 159 | abciResponse.response.ResponseBase.Data 160 | ); 161 | } 162 | 163 | async getStatus(): Promise { 164 | return await RestService.post(this.baseURL, { 165 | request: newRequest(CommonEndpoint.STATUS), 166 | }); 167 | } 168 | 169 | async getTransaction(hash: string): Promise { 170 | return await RestService.post(this.baseURL, { 171 | request: newRequest(TransactionEndpoint.TX, [hash]), 172 | }); 173 | } 174 | 175 | async sendTransaction( 176 | tx: string, 177 | endpoint: K 178 | ): Promise { 179 | const request: RPCRequest = newRequest(endpoint, [tx]); 180 | 181 | switch (endpoint) { 182 | case TransactionEndpoint.BROADCAST_TX_COMMIT: 183 | // The endpoint is a commit broadcast 184 | // (it waits for the transaction to be committed) to the chain before returning 185 | return this.broadcastTxCommit(request); 186 | case TransactionEndpoint.BROADCAST_TX_SYNC: 187 | default: 188 | return this.broadcastTxSync(request); 189 | } 190 | } 191 | 192 | private async broadcastTxSync( 193 | request: RPCRequest 194 | ): Promise { 195 | const response: BroadcastTxSyncResult = 196 | await RestService.post(this.baseURL, { 197 | request, 198 | }); 199 | 200 | // Check if there is an immediate tx-broadcast error 201 | // (originating from basic transaction checks like CheckTx) 202 | if (response.error) { 203 | const errType: string = response.error[ABCIErrorKey]; 204 | const log: string = response.Log; 205 | 206 | throw constructRequestError(errType, log); 207 | } 208 | 209 | return response; 210 | } 211 | 212 | private async broadcastTxCommit( 213 | request: RPCRequest 214 | ): Promise { 215 | const response: BroadcastTxCommitResult = 216 | await RestService.post(this.baseURL, { 217 | request, 218 | }); 219 | 220 | const { check_tx, deliver_tx } = response; 221 | 222 | // Check if there is an immediate tx-broadcast error (in CheckTx) 223 | if (check_tx.ResponseBase.Error) { 224 | const errType: string = check_tx.ResponseBase.Error[ABCIErrorKey]; 225 | const log: string = check_tx.ResponseBase.Log; 226 | 227 | throw constructRequestError(errType, log); 228 | } 229 | 230 | // Check if there is a parsing error with the transaction (in DeliverTx) 231 | if (deliver_tx.ResponseBase.Error) { 232 | const errType: string = deliver_tx.ResponseBase.Error[ABCIErrorKey]; 233 | const log: string = deliver_tx.ResponseBase.Log; 234 | 235 | throw constructRequestError(errType, log); 236 | } 237 | 238 | return response; 239 | } 240 | 241 | async waitForTransaction( 242 | hash: string, 243 | fromHeight?: number, 244 | timeout?: number 245 | ): Promise { 246 | return waitForTransaction(this, hash, fromHeight, timeout); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/provider/provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BlockInfo, 3 | BlockResult, 4 | BroadcastAsGeneric, 5 | BroadcastTransactionMap, 6 | ConsensusParams, 7 | NetworkInfo, 8 | Status, 9 | } from './types'; 10 | import { Tx } from '../proto'; 11 | 12 | /** 13 | * Read-only abstraction for accessing blockchain data 14 | */ 15 | export interface Provider { 16 | // Account-specific methods // 17 | 18 | /** 19 | * Fetches the denomination balance of the account 20 | * @param {string} address the bech32 address of the account 21 | * @param {string} [denomination=ugnot] the balance denomination 22 | * @param {number} [height=0] the height for querying. 23 | * If omitted, the latest height is used 24 | */ 25 | getBalance( 26 | address: string, 27 | denomination?: string, 28 | height?: number 29 | ): Promise; 30 | 31 | /** 32 | * Fetches the account sequence 33 | * @param {string} address the bech32 address of the account 34 | * @param {number} [height=0] the height for querying. 35 | * If omitted, the latest height is used. 36 | */ 37 | getAccountSequence(address: string, height?: number): Promise; 38 | 39 | /** 40 | * Fetches the account number. Errors out if the account 41 | * is not initialized 42 | * @param {string} address the bech32 address of the account 43 | * @param {number} [height=0] the height for querying. 44 | * If omitted, the latest height is used 45 | */ 46 | getAccountNumber(address: string, height?: number): Promise; 47 | 48 | /** 49 | * Fetches the block at the specific height, if any 50 | * @param {number} height the height for querying 51 | */ 52 | getBlock(height: number): Promise; 53 | 54 | /** 55 | * Fetches the block at the specific height, if any 56 | * @param {number} height the height for querying 57 | */ 58 | getBlockResult(height: number): Promise; 59 | 60 | /** 61 | * Fetches the latest block number from the chain 62 | */ 63 | getBlockNumber(): Promise; 64 | 65 | // Network-specific methods // 66 | 67 | /** 68 | * Fetches the network information 69 | */ 70 | getNetwork(): Promise; 71 | 72 | /** 73 | * Fetches the consensus params for the specific block height 74 | * @param {number} height the height for querying 75 | */ 76 | getConsensusParams(height: number): Promise; 77 | 78 | /** 79 | * Fetches the current node status 80 | */ 81 | getStatus(): Promise; 82 | 83 | /** 84 | * Fetches the current (recommended) average gas price 85 | */ 86 | getGasPrice(): Promise; 87 | 88 | /** 89 | * Estimates the gas limit for the transaction 90 | * @param {Tx} tx the transaction that needs estimating 91 | */ 92 | estimateGas(tx: Tx): Promise; 93 | 94 | // Transaction specific methods // 95 | 96 | /** 97 | * Sends the transaction to the node. If the type of endpoint 98 | * is a broadcast commit, waits for the transaction to be committed to the chain. 99 | * The transaction needs to be signed beforehand. 100 | * Returns the transaction broadcast result. 101 | * @param {string} tx the base64-encoded signed transaction 102 | * @param {BroadcastType} endpoint the transaction broadcast type (sync / commit) 103 | */ 104 | sendTransaction( 105 | tx: string, 106 | endpoint: K 107 | ): Promise['result']>; 108 | 109 | /** 110 | * Waits for the transaction to be committed on the chain. 111 | * NOTE: This method will not take in the fromHeight parameter once 112 | * proper transaction indexing is added - the implementation should 113 | * simply try to fetch the transaction first to see if it's included in a block 114 | * before starting to wait for it; Until then, this method should be used 115 | * in the sequence: 116 | * get latest block -> send transaction -> waitForTransaction(block before send) 117 | * @param {string} hash The transaction hash 118 | * @param {number} [fromHeight=latest] The block height used to begin the search 119 | * @param {number} [timeout=15000] Optional wait timeout in MS 120 | */ 121 | waitForTransaction( 122 | hash: string, 123 | fromHeight?: number, 124 | timeout?: number 125 | ): Promise; 126 | } 127 | -------------------------------------------------------------------------------- /src/provider/types/abci.ts: -------------------------------------------------------------------------------- 1 | export interface ABCIResponse { 2 | response: { 3 | ResponseBase: ABCIResponseBase; 4 | Key: string | null; 5 | Value: string | null; 6 | Proof: MerkleProof | null; 7 | Height: string; 8 | }; 9 | } 10 | 11 | export interface ABCIResponseBase { 12 | Error: { 13 | // ABCIErrorKey 14 | [key: string]: string; 15 | } | null; 16 | Data: string | null; 17 | Events: string | null; 18 | Log: string; 19 | Info: string; 20 | } 21 | 22 | interface MerkleProof { 23 | ops: { 24 | type: string; 25 | key: string | null; 26 | data: string | null; 27 | }[]; 28 | } 29 | 30 | export interface ABCIAccount { 31 | BaseAccount: { 32 | // the associated account address 33 | address: string; 34 | // the balance list 35 | coins: string; 36 | // the public key info 37 | public_key: { 38 | // type of public key 39 | '@type': string; 40 | // public key value 41 | value: string; 42 | } | null; 43 | // the account number (state-dependent) (decimal) 44 | account_number: string; 45 | // the account sequence / nonce (decimal) 46 | sequence: string; 47 | }; 48 | } 49 | 50 | export const ABCIErrorKey = '@type'; 51 | -------------------------------------------------------------------------------- /src/provider/types/common.ts: -------------------------------------------------------------------------------- 1 | import { ABCIResponseBase } from './abci'; 2 | import { TransactionEndpoint } from '../endpoints'; 3 | 4 | export interface NetworkInfo { 5 | // flag indicating if networking is up 6 | listening: boolean; 7 | // IDs of the listening peers 8 | listeners: string[]; 9 | // the number of peers (decimal) 10 | n_peers: string; 11 | // the IDs of connected peers 12 | peers: string[]; 13 | } 14 | 15 | export interface Status { 16 | // basic node information 17 | node_info: NodeInfo; 18 | // basic sync information 19 | sync_info: SyncInfo; 20 | // basic validator information 21 | validator_info: ValidatorInfo; 22 | } 23 | 24 | interface NodeInfo { 25 | // the version set of the node modules 26 | version_set: VersionInfo[]; 27 | // validator address @ RPC endpoint 28 | net_address: string; 29 | // the chain ID 30 | network: string; 31 | software: string; 32 | // version of the Tendermint node 33 | version: string; 34 | channels: string; 35 | // user machine name 36 | monkier: string; 37 | other: { 38 | // type of enabled tx indexing ("off" when disabled) 39 | tx_index: string; 40 | // the TCP address of the node 41 | rpc_address: string; 42 | }; 43 | } 44 | 45 | interface VersionInfo { 46 | // the name of the module 47 | Name: string; 48 | // the version of the module 49 | Version: string; 50 | // flag indicating if the module is optional 51 | Optional: boolean; 52 | } 53 | 54 | interface SyncInfo { 55 | // latest block hash 56 | latest_block_hash: string; 57 | // latest application hash 58 | latest_app_hash: string; 59 | // latest block height (decimal) 60 | latest_block_height: string; 61 | // latest block time in string format (ISO format) 62 | latest_block_time: string; 63 | // flag indicating if the node is syncing 64 | catching_up: boolean; 65 | } 66 | 67 | interface ValidatorInfo { 68 | // the address of the validator node 69 | address: string; 70 | // the validator's public key info 71 | pub_key: PublicKey; 72 | // the validator's voting power (decimal) 73 | voting_power: string; 74 | } 75 | 76 | interface PublicKey { 77 | // type of public key 78 | type: string; 79 | // public key value 80 | value: string; 81 | } 82 | 83 | export interface ConsensusParams { 84 | // the current block height 85 | block_height: string; 86 | // block consensus params 87 | consensus_params: { 88 | // the requested block 89 | Block: { 90 | // maximum tx size in bytes 91 | MaxTxBytes: string; 92 | // maximum data size in bytes 93 | MaxDataBytes: string; 94 | // maximum block size in bytes 95 | MaxBlockBytes: string; 96 | // block gas limit 97 | MaxGas: string; 98 | // block time in MS 99 | TimeIotaMS: string; 100 | }; 101 | // validator info 102 | Validator: { 103 | // public key information 104 | PubKeyTypeURLs: string[]; 105 | }; 106 | }; 107 | } 108 | 109 | export interface ConsensusState { 110 | // the current round state 111 | round_state: { 112 | // Required because of '/' in response fields (height/round/step) 113 | [key: string]: string | null | object; 114 | // the start time of the block 115 | start_time: string; 116 | // hash of the proposed block 117 | proposal_block_hash: string | null; 118 | // hash of the locked block 119 | locked_block_hash: string | null; 120 | // hash of the valid block 121 | valid_block_hash: string | null; 122 | // the vote set for the current height 123 | height_vote_set: object; 124 | }; 125 | } 126 | 127 | export interface BlockInfo { 128 | // block metadata information 129 | block_meta: BlockMeta; 130 | // combined block info 131 | block: Block; 132 | } 133 | 134 | export interface BlockMeta { 135 | // the block parts 136 | block_id: BlockID; 137 | // the block header 138 | header: BlockHeader; 139 | } 140 | 141 | export interface Block { 142 | // the block header 143 | header: BlockHeader; 144 | // data contained in the block (txs) 145 | data: { 146 | // base64 encoded transactions 147 | txs: string[] | null; 148 | }; 149 | // commit information 150 | last_commit: { 151 | // the block parts 152 | block_id: BlockID; 153 | // validator precommit information 154 | precommits: PrecommitInfo[] | null; 155 | }; 156 | } 157 | 158 | export interface BlockHeader { 159 | // version of the node 160 | version: string; 161 | // the chain ID 162 | chain_id: string; 163 | // current height (decimal) 164 | height: string; 165 | // block creation time in string format (ISO format) 166 | time: string; 167 | // number of transactions (decimal) 168 | num_txs: string; 169 | // total number of transactions in the block (decimal) 170 | total_txs: string; 171 | // the current app version 172 | app_version: string; 173 | // parent block parts 174 | last_block_id: BlockID; 175 | // parent block commit hash 176 | last_commit_hash: string | null; 177 | // data hash (txs) 178 | data_hash: string | null; 179 | // validator set hash 180 | validators_hash: string; 181 | // consensus info hash 182 | consensus_hash: string; 183 | // app info hash 184 | app_hash: string; 185 | // last results hash 186 | last_results_hash: string | null; 187 | // address of the proposer 188 | proposer_address: string; 189 | } 190 | 191 | export interface BlockID { 192 | // the hash of the ID (block) 193 | hash: string | null; 194 | // part information 195 | parts: { 196 | // total number of parts (decimal) 197 | total: string; 198 | // the hash of the part 199 | hash: string | null; 200 | }; 201 | } 202 | 203 | export interface PrecommitInfo { 204 | // type of precommit 205 | type: number; 206 | // the block height for the precommit 207 | height: string; 208 | // the round for the precommit 209 | round: string; 210 | // the block ID info 211 | block_id: BlockID; 212 | // precommit creation time (ISO format) 213 | timestamp: string; 214 | // the address of the validator who signed 215 | validator_address: string; 216 | // the index of the signer (validator) 217 | validator_index: string; 218 | // the base64 encoded signature of the signer (validator) 219 | signature: string; 220 | } 221 | 222 | export interface BlockResult { 223 | // the block height 224 | height: string; 225 | // block result info 226 | results: { 227 | // transactions contained in the block 228 | deliver_tx: DeliverTx[] | null; 229 | // end-block info 230 | end_block: EndBlock; 231 | // begin-block info 232 | begin_block: BeginBlock; 233 | }; 234 | } 235 | 236 | export interface TxResult { 237 | // the transaction hash 238 | hash: string; 239 | // tx index in the block 240 | index: number; 241 | // the block height 242 | height: string; 243 | // deliver tx response 244 | tx_result: DeliverTx; 245 | // base64 encoded transaction 246 | tx: string; 247 | } 248 | 249 | export interface DeliverTx { 250 | // the transaction ABCI response 251 | ResponseBase: ABCIResponseBase; 252 | // transaction gas limit (decimal) 253 | GasWanted: string; 254 | // transaction actual gas used (decimal) 255 | GasUsed: string; 256 | } 257 | 258 | export interface EndBlock { 259 | // the block ABCI response 260 | ResponseBase: ABCIResponseBase; 261 | // validator update info 262 | ValidatorUpdates: string | null; 263 | // consensus params 264 | ConsensusParams: string | null; 265 | // block events 266 | Events: string | null; 267 | } 268 | 269 | export interface BeginBlock { 270 | // the block ABCI response 271 | ResponseBase: ABCIResponseBase; 272 | } 273 | 274 | export interface BroadcastTxSyncResult { 275 | error: { 276 | // ABCIErrorKey 277 | [key: string]: string; 278 | } | null; 279 | data: string | null; 280 | Log: string; 281 | 282 | hash: string; 283 | } 284 | 285 | export interface BroadcastTxCommitResult { 286 | check_tx: DeliverTx; 287 | deliver_tx: DeliverTx; 288 | hash: string; 289 | height: string; // decimal number 290 | } 291 | 292 | export type BroadcastType = 293 | | TransactionEndpoint.BROADCAST_TX_SYNC 294 | | TransactionEndpoint.BROADCAST_TX_COMMIT; 295 | 296 | export type BroadcastTransactionSync = { 297 | endpoint: TransactionEndpoint.BROADCAST_TX_SYNC; 298 | result: BroadcastTxSyncResult; 299 | }; 300 | 301 | export type BroadcastTransactionCommit = { 302 | endpoint: TransactionEndpoint.BROADCAST_TX_COMMIT; 303 | result: BroadcastTxCommitResult; 304 | }; 305 | 306 | export type BroadcastTransactionMap = { 307 | [TransactionEndpoint.BROADCAST_TX_COMMIT]: BroadcastTransactionCommit; 308 | [TransactionEndpoint.BROADCAST_TX_SYNC]: BroadcastTransactionSync; 309 | }; 310 | 311 | export type BroadcastAsGeneric< 312 | K extends keyof BroadcastTransactionMap = keyof BroadcastTransactionMap, 313 | > = { 314 | [P in K]: BroadcastTransactionMap[P]; 315 | }[K]; 316 | -------------------------------------------------------------------------------- /src/provider/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './abci'; 2 | export * from './common'; 3 | export * from './jsonrpc'; 4 | -------------------------------------------------------------------------------- /src/provider/types/jsonrpc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The base JSON-RPC 2.0 request 3 | */ 4 | export interface RPCRequest { 5 | jsonrpc: string; 6 | id: string | number; 7 | method: string; 8 | 9 | params?: any[]; 10 | } 11 | 12 | /** 13 | * The base JSON-RPC 2.0 response 14 | */ 15 | export interface RPCResponse { 16 | jsonrpc: string; 17 | id: string | number; 18 | 19 | result?: Result; 20 | error?: RPCError; 21 | } 22 | 23 | /** 24 | * The base JSON-RPC 2.0 typed response error 25 | */ 26 | export interface RPCError { 27 | code: number; 28 | message: string; 29 | 30 | data?: any; 31 | } 32 | -------------------------------------------------------------------------------- /src/provider/utility/errors.utility.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GasOverflowError, 3 | InsufficientCoinsError, 4 | InsufficientFeeError, 5 | InsufficientFundsError, 6 | InternalError, 7 | InvalidAddressError, 8 | InvalidCoinsError, 9 | InvalidGasWantedError, 10 | InvalidPubKeyError, 11 | InvalidSequenceError, 12 | MemoTooLargeError, 13 | NoSignaturesError, 14 | OutOfGasError, 15 | TM2Error, 16 | TooManySignaturesError, 17 | TxDecodeError, 18 | UnauthorizedError, 19 | UnknownAddressError, 20 | UnknownRequestError, 21 | } from '../errors'; 22 | 23 | /** 24 | * Constructs the appropriate Tendermint2 25 | * error based on the error ID. 26 | * Error IDs retrieved from: 27 | * https://github.com/gnolang/gno/blob/64f0fd0fa44021a076e1453b1767fbc914ed3b66/tm2/pkg/std/package.go#L20C1-L38 28 | * @param {string} errorID the proto ID of the error 29 | * @param {string} [log] the log associated with the error, if any 30 | * @returns {TM2Error} 31 | */ 32 | export const constructRequestError = ( 33 | errorID: string, 34 | log?: string 35 | ): TM2Error => { 36 | switch (errorID) { 37 | case '/std.InternalError': 38 | return new InternalError(log); 39 | case '/std.TxDecodeError': 40 | return new TxDecodeError(log); 41 | case '/std.InvalidSequenceError': 42 | return new InvalidSequenceError(log); 43 | case '/std.UnauthorizedError': 44 | return new UnauthorizedError(log); 45 | case '/std.InsufficientFundsError': 46 | return new InsufficientFundsError(log); 47 | case '/std.UnknownRequestError': 48 | return new UnknownRequestError(log); 49 | case '/std.InvalidAddressError': 50 | return new InvalidAddressError(log); 51 | case '/std.UnknownAddressError': 52 | return new UnknownAddressError(log); 53 | case '/std.InvalidPubKeyError': 54 | return new InvalidPubKeyError(log); 55 | case '/std.InsufficientCoinsError': 56 | return new InsufficientCoinsError(log); 57 | case '/std.InvalidCoinsError': 58 | return new InvalidCoinsError(log); 59 | case '/std.InvalidGasWantedError': 60 | return new InvalidGasWantedError(log); 61 | case '/std.OutOfGasError': 62 | return new OutOfGasError(log); 63 | case '/std.MemoTooLargeError': 64 | return new MemoTooLargeError(log); 65 | case '/std.InsufficientFeeError': 66 | return new InsufficientFeeError(log); 67 | case '/std.TooManySignaturesError': 68 | return new TooManySignaturesError(log); 69 | case '/std.NoSignaturesError': 70 | return new NoSignaturesError(log); 71 | case '/std.GasOverflowError': 72 | return new GasOverflowError(log); 73 | default: 74 | return new TM2Error(`unknown error: ${errorID}`, log); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/provider/utility/index.ts: -------------------------------------------------------------------------------- 1 | export * from './provider.utility'; 2 | export * from './requests.utility'; 3 | -------------------------------------------------------------------------------- /src/provider/utility/provider.utility.ts: -------------------------------------------------------------------------------- 1 | import { sha256 } from '@cosmjs/crypto'; 2 | import { Tx } from '../../proto'; 3 | import { ResponseDeliverTx } from '../../proto/tm2/abci'; 4 | import { Provider } from '../provider'; 5 | import { ABCIAccount, ABCIErrorKey, ABCIResponse, BlockInfo } from '../types'; 6 | import { constructRequestError } from './errors.utility'; 7 | import { 8 | base64ToUint8Array, 9 | parseABCI, 10 | parseProto, 11 | uint8ArrayToBase64, 12 | } from './requests.utility'; 13 | 14 | /** 15 | * Extracts the specific balance denomination from the ABCI response 16 | * @param {string | null} abciData the base64-encoded ABCI data 17 | * @param {string} denomination the required denomination 18 | */ 19 | export const extractBalanceFromResponse = ( 20 | abciData: string | null, 21 | denomination: string 22 | ): number => { 23 | // Make sure the response is initialized 24 | if (!abciData) { 25 | return 0; 26 | } 27 | 28 | // Extract the balances 29 | const balancesRaw = Buffer.from(abciData, 'base64') 30 | .toString() 31 | .replace(/"/gi, ''); 32 | 33 | // Find the correct balance denomination 34 | const balances: string[] = balancesRaw.split(','); 35 | if (balances.length < 1) { 36 | return 0; 37 | } 38 | 39 | // Find the correct denomination 40 | const pattern = new RegExp(`^(\\d+)${denomination}$`); 41 | for (const balance of balances) { 42 | const match = balance.match(pattern); 43 | if (match) { 44 | return parseInt(match[1], 10); 45 | } 46 | } 47 | 48 | return 0; 49 | }; 50 | 51 | /** 52 | * Extracts the account sequence from the ABCI response 53 | * @param {string | null} abciData the base64-encoded ABCI data 54 | */ 55 | export const extractSequenceFromResponse = ( 56 | abciData: string | null 57 | ): number => { 58 | // Make sure the response is initialized 59 | if (!abciData) { 60 | return 0; 61 | } 62 | 63 | try { 64 | // Parse the account 65 | const account: ABCIAccount = parseABCI(abciData); 66 | 67 | return parseInt(account.BaseAccount.sequence, 10); 68 | } catch (e) { 69 | // unused case 70 | } 71 | 72 | // Account not initialized, 73 | // return default value (0) 74 | return 0; 75 | }; 76 | 77 | /** 78 | * Extracts the account number from the ABCI response 79 | * @param {string | null} abciData the base64-encoded ABCI data 80 | */ 81 | export const extractAccountNumberFromResponse = ( 82 | abciData: string | null 83 | ): number => { 84 | // Make sure the response is initialized 85 | if (!abciData) { 86 | throw new Error('account is not initialized'); 87 | } 88 | 89 | try { 90 | // Parse the account 91 | const account: ABCIAccount = parseABCI(abciData); 92 | 93 | return parseInt(account.BaseAccount.account_number, 10); 94 | } catch (e) { 95 | throw new Error('account is not initialized'); 96 | } 97 | }; 98 | 99 | /** 100 | * Extracts the simulate transaction response from the ABCI response value 101 | * @param {string | null} abciData the base64-encoded ResponseDeliverTx proto message 102 | */ 103 | export const extractSimulateFromResponse = ( 104 | abciResponse: ABCIResponse | null 105 | ): ResponseDeliverTx => { 106 | // Make sure the response is initialized 107 | if (!abciResponse) { 108 | throw new Error('abci data is not initialized'); 109 | } 110 | 111 | const error = abciResponse.response?.ResponseBase?.Error; 112 | if (error && error[ABCIErrorKey]) { 113 | throw constructRequestError(error[ABCIErrorKey]); 114 | } 115 | 116 | const value = abciResponse.response.Value; 117 | if (!value) { 118 | throw new Error('abci data is not initialized'); 119 | } 120 | 121 | try { 122 | return parseProto(value, ResponseDeliverTx.decode); 123 | } catch (e) { 124 | throw new Error('unable to parse simulate response'); 125 | } 126 | }; 127 | 128 | /** 129 | * Waits for the transaction to be committed to a block in the chain 130 | * of the specified provider. This helper does a search for incoming blocks 131 | * and checks if a transaction 132 | * @param {Provider} provider the provider instance 133 | * @param {string} hash the base64-encoded hash of the transaction 134 | * @param {number} [fromHeight=latest] the starting height for the search. If omitted, it is the latest block in the chain 135 | * @param {number} [timeout=15000] the timeout in MS for the search 136 | */ 137 | export const waitForTransaction = async ( 138 | provider: Provider, 139 | hash: string, 140 | fromHeight?: number, 141 | timeout?: number 142 | ): Promise => { 143 | return new Promise(async (resolve, reject) => { 144 | // Fetch the starting point 145 | let currentHeight = fromHeight 146 | ? fromHeight 147 | : await provider.getBlockNumber(); 148 | 149 | const exitTimeout = timeout ? timeout : 15000; 150 | 151 | const fetchInterval = setInterval(async () => { 152 | // Fetch the latest block height 153 | const latestHeight = await provider.getBlockNumber(); 154 | 155 | if (latestHeight < currentHeight) { 156 | // No need to parse older blocks 157 | return; 158 | } 159 | 160 | for (let blockNum = currentHeight; blockNum <= latestHeight; blockNum++) { 161 | // Fetch the block from the chain 162 | const block: BlockInfo = await provider.getBlock(blockNum); 163 | 164 | // Check if there are any transactions at all in the block 165 | if (!block.block.data.txs || block.block.data.txs.length == 0) { 166 | continue; 167 | } 168 | 169 | // Find the transaction among the block transactions 170 | for (const tx of block.block.data.txs) { 171 | // Decode the base-64 transaction 172 | const txRaw = base64ToUint8Array(tx); 173 | 174 | // Calculate the transaction hash 175 | const txHash = sha256(txRaw); 176 | 177 | if (uint8ArrayToBase64(txHash) == hash) { 178 | // Clear the interval 179 | clearInterval(fetchInterval); 180 | 181 | // Decode the transaction from amino 182 | resolve(Tx.decode(txRaw)); 183 | } 184 | } 185 | } 186 | 187 | currentHeight = latestHeight + 1; 188 | }, 1000); 189 | 190 | setTimeout(() => { 191 | // Clear the fetch interval 192 | clearInterval(fetchInterval); 193 | 194 | reject('transaction fetch timeout'); 195 | }, exitTimeout); 196 | }); 197 | }; 198 | -------------------------------------------------------------------------------- /src/provider/utility/requests.utility.ts: -------------------------------------------------------------------------------- 1 | import { BinaryReader } from '@bufbuild/protobuf/wire'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { RPCError, RPCRequest, RPCResponse } from '../types'; 4 | 5 | // The version of the supported JSON-RPC protocol 6 | const standardVersion = '2.0'; 7 | 8 | /** 9 | * Creates a new JSON-RPC 2.0 request 10 | * @param {string} method the requested method 11 | * @param {string[]} [params] the requested params, if any 12 | */ 13 | export const newRequest = (method: string, params?: any[]): RPCRequest => { 14 | return { 15 | // the ID of the request is not that relevant for this helper method; 16 | // for finer ID control, instantiate the request object directly 17 | id: uuidv4(), 18 | jsonrpc: standardVersion, 19 | method: method, 20 | params: params, 21 | }; 22 | }; 23 | 24 | /** 25 | * Creates a new JSON-RPC 2.0 response 26 | * @param {Result} result the response result, if any 27 | * @param {RPCError} error the response error, if any 28 | */ 29 | export const newResponse = ( 30 | result?: Result, 31 | error?: RPCError 32 | ): RPCResponse => { 33 | return { 34 | id: uuidv4(), 35 | jsonrpc: standardVersion, 36 | result: result, 37 | error: error, 38 | }; 39 | }; 40 | 41 | /** 42 | * Parses the base64 encoded ABCI JSON into a concrete type 43 | * @param {string} data the base64-encoded JSON 44 | */ 45 | export const parseABCI = (data: string): Result => { 46 | const jsonData: string = Buffer.from(data, 'base64').toString(); 47 | const parsedData: Result | null = JSON.parse(jsonData); 48 | 49 | if (!parsedData) { 50 | throw new Error('unable to parse JSON response'); 51 | } 52 | 53 | return parsedData; 54 | }; 55 | 56 | export const parseProto = ( 57 | data: string, 58 | decodeFn: (input: BinaryReader | Uint8Array, length?: number) => T 59 | ) => { 60 | const protoData = decodeFn(Buffer.from(data, 'base64')); 61 | 62 | return protoData; 63 | }; 64 | 65 | /** 66 | * Converts a string into base64 representation 67 | * @param {string} str the raw string 68 | */ 69 | export const stringToBase64 = (str: string): string => { 70 | const buffer = Buffer.from(str, 'utf-8'); 71 | 72 | return buffer.toString('base64'); 73 | }; 74 | 75 | /** 76 | * Converts a base64 string into a Uint8Array representation 77 | * @param {string} str the base64-encoded string 78 | */ 79 | export const base64ToUint8Array = (str: string): Uint8Array => { 80 | const buffer = Buffer.from(str, 'base64'); 81 | 82 | return new Uint8Array(buffer); 83 | }; 84 | 85 | /** 86 | * Converts a Uint8Array into base64 representation 87 | * @param {Uint8Array} data the Uint8Array to be encoded 88 | */ 89 | export const uint8ArrayToBase64 = (data: Uint8Array): string => { 90 | return Buffer.from(data).toString('base64'); 91 | }; 92 | -------------------------------------------------------------------------------- /src/provider/websocket/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ws'; 2 | -------------------------------------------------------------------------------- /src/provider/websocket/ws.test.ts: -------------------------------------------------------------------------------- 1 | import { sha256 } from '@cosmjs/crypto'; 2 | import WS from 'jest-websocket-mock'; 3 | import Long from 'long'; 4 | import { Tx } from '../../proto'; 5 | import { CommonEndpoint, TransactionEndpoint } from '../endpoints'; 6 | import { TM2Error } from '../errors'; 7 | import { UnauthorizedErrorMessage } from '../errors/messages'; 8 | import { 9 | ABCIAccount, 10 | ABCIErrorKey, 11 | ABCIResponse, 12 | ABCIResponseBase, 13 | BeginBlock, 14 | BlockInfo, 15 | BlockResult, 16 | BroadcastTxSyncResult, 17 | ConsensusParams, 18 | EndBlock, 19 | NetworkInfo, 20 | Status, 21 | } from '../types'; 22 | import { newResponse, stringToBase64, uint8ArrayToBase64 } from '../utility'; 23 | import { WSProvider } from './ws'; 24 | 25 | describe('WS Provider', () => { 26 | const wsPort = 8545; 27 | const wsHost = 'localhost'; 28 | const wsURL = `ws://${wsHost}:${wsPort}`; 29 | 30 | let server: WS; 31 | let wsProvider: WSProvider; 32 | 33 | const mockABCIResponse = (response: string): ABCIResponse => { 34 | return { 35 | response: { 36 | ResponseBase: { 37 | Log: '', 38 | Info: '', 39 | Data: stringToBase64(response), 40 | Error: null, 41 | Events: null, 42 | }, 43 | Key: null, 44 | Value: null, 45 | Proof: null, 46 | Height: '', 47 | }, 48 | }; 49 | }; 50 | 51 | /** 52 | * Sets up the test response handler (single-response) 53 | * @param {WebSocketServer} wss the websocket server returning data 54 | * @param {Type} testData the test data being returned to the client 55 | */ 56 | const setHandler = async (testData: Type) => { 57 | server.on('connection', (socket) => { 58 | socket.on('message', (data) => { 59 | const request = JSON.parse(data.toString()); 60 | const response = newResponse(testData); 61 | response.id = request.id; 62 | 63 | socket.send(JSON.stringify(response)); 64 | }); 65 | }); 66 | 67 | await server.connected; 68 | }; 69 | 70 | beforeEach(() => { 71 | server = new WS(wsURL); 72 | wsProvider = new WSProvider(wsURL); 73 | }); 74 | 75 | afterEach(() => { 76 | wsProvider.closeConnection(); 77 | WS.clean(); 78 | }); 79 | 80 | test('estimateGas', async () => { 81 | const tx = Tx.fromJSON({ 82 | signatures: [], 83 | fee: { 84 | gasFee: '', 85 | gasWanted: new Long(0), 86 | }, 87 | messages: [], 88 | memo: '', 89 | }); 90 | const expectedEstimation = 44900; 91 | 92 | const mockSimulateResponseVale = 93 | 'CiMiIW1zZzowLHN1Y2Nlc3M6dHJ1ZSxsb2c6LGV2ZW50czpbXRCAiXoYyL0F'; 94 | 95 | const mockABCIResponse: ABCIResponse = { 96 | response: { 97 | Height: '', 98 | Key: '', 99 | Proof: null, 100 | Value: mockSimulateResponseVale, 101 | ResponseBase: { 102 | Log: '', 103 | Info: '', 104 | Error: null, 105 | Events: null, 106 | Data: '', 107 | }, 108 | }, 109 | }; 110 | 111 | // Set the response 112 | await setHandler(mockABCIResponse); 113 | 114 | const estimation = await wsProvider.estimateGas(tx); 115 | 116 | expect(estimation).toEqual(expectedEstimation); 117 | }); 118 | 119 | test('getNetwork', async () => { 120 | const mockInfo: NetworkInfo = { 121 | listening: false, 122 | listeners: [], 123 | n_peers: '0', 124 | peers: [], 125 | }; 126 | 127 | // Set the response 128 | await setHandler(mockInfo); 129 | 130 | const info: NetworkInfo = await wsProvider.getNetwork(); 131 | expect(info).toEqual(mockInfo); 132 | }); 133 | 134 | const getEmptyStatus = (): Status => { 135 | return { 136 | node_info: { 137 | version_set: [], 138 | net_address: '', 139 | network: '', 140 | software: '', 141 | version: '', 142 | channels: '', 143 | monkier: '', 144 | other: { 145 | tx_index: '', 146 | rpc_address: '', 147 | }, 148 | }, 149 | sync_info: { 150 | latest_block_hash: '', 151 | latest_app_hash: '', 152 | latest_block_height: '', 153 | latest_block_time: '', 154 | catching_up: false, 155 | }, 156 | validator_info: { 157 | address: '', 158 | pub_key: { 159 | type: '', 160 | value: '', 161 | }, 162 | voting_power: '', 163 | }, 164 | }; 165 | }; 166 | 167 | test('getStatus', async () => { 168 | const mockStatus: Status = getEmptyStatus(); 169 | mockStatus.validator_info.address = 'address'; 170 | 171 | // Set the response 172 | await setHandler(mockStatus); 173 | 174 | const status: Status = await wsProvider.getStatus(); 175 | expect(status).toEqual(status); 176 | }); 177 | 178 | test('getConsensusParams', async () => { 179 | const mockParams: ConsensusParams = { 180 | block_height: '', 181 | consensus_params: { 182 | Block: { 183 | MaxTxBytes: '', 184 | MaxDataBytes: '', 185 | MaxBlockBytes: '', 186 | MaxGas: '', 187 | TimeIotaMS: '', 188 | }, 189 | Validator: { 190 | PubKeyTypeURLs: [], 191 | }, 192 | }, 193 | }; 194 | 195 | // Set the response 196 | await setHandler(mockParams); 197 | 198 | const params: ConsensusParams = await wsProvider.getConsensusParams(1); 199 | expect(params).toEqual(mockParams); 200 | }); 201 | 202 | describe('getSequence', () => { 203 | const validAccount: ABCIAccount = { 204 | BaseAccount: { 205 | address: 'random address', 206 | coins: '', 207 | public_key: null, 208 | account_number: '0', 209 | sequence: '10', 210 | }, 211 | }; 212 | 213 | test.each([ 214 | [ 215 | JSON.stringify(validAccount), 216 | parseInt(validAccount.BaseAccount.sequence, 10), 217 | ], // account exists 218 | ['null', 0], // account doesn't exist 219 | ])('case %#', async (response, expected) => { 220 | const mockResponse: ABCIResponse = mockABCIResponse(response); 221 | 222 | // Set the response 223 | await setHandler(mockResponse); 224 | 225 | const sequence: number = await wsProvider.getAccountSequence('address'); 226 | expect(sequence).toBe(expected); 227 | }); 228 | }); 229 | 230 | describe('getAccountNumber', () => { 231 | const validAccount: ABCIAccount = { 232 | BaseAccount: { 233 | address: 'random address', 234 | coins: '', 235 | public_key: null, 236 | account_number: '10', 237 | sequence: '0', 238 | }, 239 | }; 240 | 241 | test.each([ 242 | [ 243 | JSON.stringify(validAccount), 244 | parseInt(validAccount.BaseAccount.account_number, 10), 245 | ], // account exists 246 | ['null', 0], // account doesn't exist 247 | ])('case %#', async (response, expected) => { 248 | const mockResponse: ABCIResponse = mockABCIResponse(response); 249 | 250 | // Set the response 251 | await setHandler(mockResponse); 252 | 253 | try { 254 | const accountNumber: number = 255 | await wsProvider.getAccountNumber('address'); 256 | expect(accountNumber).toBe(expected); 257 | } catch (e) { 258 | expect((e as Error).message).toContain('account is not initialized'); 259 | } 260 | }); 261 | }); 262 | 263 | describe('getBalance', () => { 264 | const denomination = 'atom'; 265 | test.each([ 266 | ['"5gnot,100atom"', 100], // balance found 267 | ['"5universe"', 0], // balance not found 268 | ['""', 0], // account doesn't exist 269 | ])('case %#', async (existing, expected) => { 270 | const mockResponse: ABCIResponse = mockABCIResponse(existing); 271 | 272 | // Set the response 273 | await setHandler(mockResponse); 274 | 275 | const balance: number = await wsProvider.getBalance( 276 | 'address', 277 | denomination 278 | ); 279 | expect(balance).toBe(expected); 280 | }); 281 | }); 282 | 283 | test('getBlockNumber', async () => { 284 | const expectedBlockNumber = 10; 285 | const mockStatus: Status = getEmptyStatus(); 286 | mockStatus.sync_info.latest_block_height = `${expectedBlockNumber}`; 287 | 288 | // Set the response 289 | await setHandler(mockStatus); 290 | 291 | const blockNumber: number = await wsProvider.getBlockNumber(); 292 | expect(blockNumber).toBe(expectedBlockNumber); 293 | }); 294 | 295 | describe('sendTransaction', () => { 296 | const validResult: BroadcastTxSyncResult = { 297 | error: null, 298 | data: null, 299 | Log: '', 300 | hash: 'hash123', 301 | }; 302 | 303 | const mockError = '/std.UnauthorizedError'; 304 | const mockLog = 'random error message'; 305 | const invalidResult: BroadcastTxSyncResult = { 306 | error: { 307 | [ABCIErrorKey]: mockError, 308 | }, 309 | data: null, 310 | Log: mockLog, 311 | hash: '', 312 | }; 313 | 314 | test.each([ 315 | [validResult, validResult.hash, '', ''], // no error 316 | [invalidResult, invalidResult.hash, UnauthorizedErrorMessage, mockLog], // error out 317 | ])('case %#', async (response, expectedHash, expectedErr, expectedLog) => { 318 | await setHandler(response); 319 | 320 | try { 321 | const tx = await wsProvider.sendTransaction( 322 | 'encoded tx', 323 | TransactionEndpoint.BROADCAST_TX_SYNC 324 | ); 325 | 326 | expect(tx.hash).toEqual(expectedHash); 327 | 328 | if (expectedErr != '') { 329 | fail('expected error'); 330 | } 331 | } catch (e) { 332 | expect((e as Error).message).toBe(expectedErr); 333 | expect((e as TM2Error).log).toBe(expectedLog); 334 | } 335 | }); 336 | }); 337 | 338 | const getEmptyBlockInfo = (): BlockInfo => { 339 | const emptyHeader = { 340 | version: '', 341 | chain_id: '', 342 | height: '', 343 | time: '', 344 | num_txs: '', 345 | total_txs: '', 346 | app_version: '', 347 | last_block_id: { 348 | hash: null, 349 | parts: { 350 | total: '', 351 | hash: null, 352 | }, 353 | }, 354 | last_commit_hash: '', 355 | data_hash: '', 356 | validators_hash: '', 357 | consensus_hash: '', 358 | app_hash: '', 359 | last_results_hash: '', 360 | proposer_address: '', 361 | }; 362 | 363 | const emptyBlockID = { 364 | hash: null, 365 | parts: { 366 | total: '', 367 | hash: null, 368 | }, 369 | }; 370 | 371 | return { 372 | block_meta: { 373 | block_id: emptyBlockID, 374 | header: emptyHeader, 375 | }, 376 | block: { 377 | header: emptyHeader, 378 | data: { 379 | txs: null, 380 | }, 381 | last_commit: { 382 | block_id: emptyBlockID, 383 | precommits: null, 384 | }, 385 | }, 386 | }; 387 | }; 388 | 389 | test('getBlock', async () => { 390 | const mockInfo: BlockInfo = getEmptyBlockInfo(); 391 | 392 | // Set the response 393 | await setHandler(mockInfo); 394 | 395 | const result: BlockInfo = await wsProvider.getBlock(0); 396 | expect(result).toEqual(mockInfo); 397 | }); 398 | 399 | const getEmptyBlockResult = (): BlockResult => { 400 | const emptyResponseBase: ABCIResponseBase = { 401 | Error: null, 402 | Data: null, 403 | Events: null, 404 | Log: '', 405 | Info: '', 406 | }; 407 | 408 | const emptyEndBlock: EndBlock = { 409 | ResponseBase: emptyResponseBase, 410 | ValidatorUpdates: null, 411 | ConsensusParams: null, 412 | Events: null, 413 | }; 414 | 415 | const emptyStartBlock: BeginBlock = { 416 | ResponseBase: emptyResponseBase, 417 | }; 418 | 419 | return { 420 | height: '', 421 | results: { 422 | deliver_tx: null, 423 | end_block: emptyEndBlock, 424 | begin_block: emptyStartBlock, 425 | }, 426 | }; 427 | }; 428 | 429 | test('getBlockResult', async () => { 430 | const mockResult: BlockResult = getEmptyBlockResult(); 431 | 432 | // Set the response 433 | await setHandler(mockResult); 434 | 435 | const result: BlockResult = await wsProvider.getBlockResult(0); 436 | expect(result).toEqual(mockResult); 437 | }); 438 | 439 | test('waitForTransaction', async () => { 440 | const emptyBlock: BlockInfo = getEmptyBlockInfo(); 441 | emptyBlock.block.data = { 442 | txs: [], 443 | }; 444 | 445 | const tx: Tx = { 446 | messages: [], 447 | signatures: [], 448 | memo: 'tx memo', 449 | }; 450 | 451 | const encodedTx = Tx.encode(tx).finish(); 452 | const txHash = sha256(encodedTx); 453 | 454 | const filledBlock: BlockInfo = getEmptyBlockInfo(); 455 | filledBlock.block.data = { 456 | txs: [uint8ArrayToBase64(encodedTx)], 457 | }; 458 | 459 | const latestBlock = 5; 460 | const startBlock = latestBlock - 2; 461 | 462 | const mockStatus: Status = getEmptyStatus(); 463 | mockStatus.sync_info.latest_block_height = `${latestBlock}`; 464 | 465 | const responseMap: Map = new Map([ 466 | [latestBlock, filledBlock], 467 | [latestBlock - 1, emptyBlock], 468 | [startBlock, emptyBlock], 469 | ]); 470 | 471 | // Set the response 472 | server.on('connection', (socket) => { 473 | socket.on('message', (data) => { 474 | const request = JSON.parse(data.toString()); 475 | 476 | if (request.method == CommonEndpoint.STATUS) { 477 | const response = newResponse(mockStatus); 478 | response.id = request.id; 479 | 480 | socket.send(JSON.stringify(response)); 481 | 482 | return; 483 | } 484 | 485 | if (!request.params) { 486 | return; 487 | } 488 | 489 | const blockNum: number = +(request.params[0] as string[]); 490 | const info = responseMap.get(blockNum); 491 | 492 | const response = newResponse(info); 493 | response.id = request.id; 494 | 495 | socket.send(JSON.stringify(response)); 496 | }); 497 | }); 498 | 499 | await server.connected; 500 | 501 | const receivedTx: Tx = await wsProvider.waitForTransaction( 502 | uint8ArrayToBase64(txHash), 503 | startBlock 504 | ); 505 | expect(receivedTx).toEqual(tx); 506 | }); 507 | }); 508 | -------------------------------------------------------------------------------- /src/provider/websocket/ws.ts: -------------------------------------------------------------------------------- 1 | import { Tx } from '../../proto'; 2 | import { 3 | ABCIEndpoint, 4 | BlockEndpoint, 5 | CommonEndpoint, 6 | ConsensusEndpoint, 7 | TransactionEndpoint, 8 | } from '../endpoints'; 9 | import { Provider } from '../provider'; 10 | import { 11 | ABCIErrorKey, 12 | ABCIResponse, 13 | BlockInfo, 14 | BlockResult, 15 | BroadcastTransactionMap, 16 | BroadcastTxCommitResult, 17 | BroadcastTxSyncResult, 18 | ConsensusParams, 19 | NetworkInfo, 20 | RPCRequest, 21 | RPCResponse, 22 | Status, 23 | TxResult, 24 | } from '../types'; 25 | import { 26 | extractAccountNumberFromResponse, 27 | extractBalanceFromResponse, 28 | extractSequenceFromResponse, 29 | extractSimulateFromResponse, 30 | newRequest, 31 | uint8ArrayToBase64, 32 | waitForTransaction, 33 | } from '../utility'; 34 | import { constructRequestError } from '../utility/errors.utility'; 35 | 36 | /** 37 | * Provider based on WS JSON-RPC HTTP requests 38 | */ 39 | export class WSProvider implements Provider { 40 | protected ws: WebSocket; // the persistent WS connection 41 | protected readonly requestMap: Map< 42 | number | string, 43 | { 44 | resolve: (response: RPCResponse) => void; 45 | reject: (reason: Error) => void; 46 | timeout: NodeJS.Timeout; 47 | } 48 | > = new Map(); // callback method map for the individual endpoints 49 | protected requestTimeout = 15000; // 15s 50 | 51 | /** 52 | * Creates a new instance of the {@link WSProvider} 53 | * @param {string} baseURL the WS URL of the node 54 | * @param {number} requestTimeout the timeout for the WS request (in MS) 55 | */ 56 | constructor(baseURL: string, requestTimeout?: number) { 57 | this.ws = new WebSocket(baseURL); 58 | 59 | this.ws.addEventListener('message', (event) => { 60 | const response = JSON.parse(event.data as string) as RPCResponse; 61 | const request = this.requestMap.get(response.id); 62 | if (request) { 63 | this.requestMap.delete(response.id); 64 | clearTimeout(request.timeout); 65 | 66 | request.resolve(response); 67 | } 68 | 69 | // Set the default timeout 70 | this.requestTimeout = requestTimeout ? requestTimeout : 15000; 71 | }); 72 | } 73 | 74 | /** 75 | * Closes the WS connection. Required when done working 76 | * with the WS provider 77 | */ 78 | closeConnection() { 79 | this.ws.close(); 80 | } 81 | 82 | /** 83 | * Sends a request to the WS connection, and resolves 84 | * upon receiving the response 85 | * @param {RPCRequest} request the RPC request 86 | */ 87 | async sendRequest(request: RPCRequest): Promise> { 88 | // Make sure the connection is open 89 | if (this.ws.readyState != WebSocket.OPEN) { 90 | await this.waitForOpenConnection(); 91 | } 92 | 93 | // The promise will resolve as soon as the response is received 94 | const promise = new Promise>((resolve, reject) => { 95 | const timeout = setTimeout(() => { 96 | this.requestMap.delete(request.id); 97 | 98 | reject(new Error('Request timed out')); 99 | }, this.requestTimeout); 100 | 101 | this.requestMap.set(request.id, { resolve, reject, timeout }); 102 | }); 103 | 104 | this.ws.send(JSON.stringify(request)); 105 | 106 | return promise; 107 | } 108 | 109 | /** 110 | * Parses the result from the response 111 | * @param {RPCResponse} response the response to be parsed 112 | */ 113 | parseResponse(response: RPCResponse): Result { 114 | if (!response) { 115 | throw new Error('invalid response'); 116 | } 117 | 118 | if (response.error) { 119 | throw new Error(response.error?.message); 120 | } 121 | 122 | if (!response.result) { 123 | throw new Error('invalid response returned'); 124 | } 125 | 126 | return response.result; 127 | } 128 | 129 | /** 130 | * Waits for the WS connection to be established 131 | */ 132 | waitForOpenConnection = (): Promise => { 133 | return new Promise((resolve, reject) => { 134 | const maxNumberOfAttempts = 20; 135 | const intervalTime = 500; // ms 136 | 137 | let currentAttempt = 0; 138 | const interval = setInterval(() => { 139 | if (this.ws.readyState === WebSocket.OPEN) { 140 | clearInterval(interval); 141 | resolve(null); 142 | } 143 | 144 | currentAttempt++; 145 | if (currentAttempt >= maxNumberOfAttempts) { 146 | clearInterval(interval); 147 | reject(new Error('Unable to establish WS connection')); 148 | } 149 | }, intervalTime); 150 | }); 151 | }; 152 | 153 | async estimateGas(tx: Tx): Promise { 154 | const encodedTx = uint8ArrayToBase64(Tx.encode(tx).finish()); 155 | const response = await this.sendRequest( 156 | newRequest(ABCIEndpoint.ABCI_QUERY, [ 157 | `.app/simulate`, 158 | `${encodedTx}`, 159 | '0', // Height; not supported > 0 for now 160 | false, 161 | ]) 162 | ); 163 | 164 | // Parse the response 165 | const abciResponse = this.parseResponse(response); 166 | 167 | const simulateResult = extractSimulateFromResponse(abciResponse); 168 | 169 | const resultErrorKey = simulateResult.responseBase?.error?.typeUrl; 170 | if (resultErrorKey) { 171 | throw constructRequestError(resultErrorKey); 172 | } 173 | 174 | return simulateResult.gasUsed.toInt(); 175 | } 176 | 177 | async getBalance( 178 | address: string, 179 | denomination?: string, 180 | height?: number 181 | ): Promise { 182 | const response = await this.sendRequest( 183 | newRequest(ABCIEndpoint.ABCI_QUERY, [ 184 | `bank/balances/${address}`, 185 | '', 186 | '0', // Height; not supported > 0 for now 187 | false, 188 | ]) 189 | ); 190 | 191 | // Parse the response 192 | const abciResponse = this.parseResponse(response); 193 | 194 | return extractBalanceFromResponse( 195 | abciResponse.response.ResponseBase.Data, 196 | denomination ? denomination : 'ugnot' 197 | ); 198 | } 199 | 200 | async getBlock(height: number): Promise { 201 | const response = await this.sendRequest( 202 | newRequest(BlockEndpoint.BLOCK, [height.toString()]) 203 | ); 204 | 205 | return this.parseResponse(response); 206 | } 207 | 208 | async getBlockResult(height: number): Promise { 209 | const response = await this.sendRequest( 210 | newRequest(BlockEndpoint.BLOCK_RESULTS, [height.toString()]) 211 | ); 212 | 213 | return this.parseResponse(response); 214 | } 215 | 216 | async getBlockNumber(): Promise { 217 | // Fetch the status for the latest info 218 | const status = await this.getStatus(); 219 | 220 | return parseInt(status.sync_info.latest_block_height); 221 | } 222 | 223 | async getConsensusParams(height: number): Promise { 224 | const response = await this.sendRequest( 225 | newRequest(ConsensusEndpoint.CONSENSUS_PARAMS, [height.toString()]) 226 | ); 227 | 228 | return this.parseResponse(response); 229 | } 230 | 231 | getGasPrice(): Promise { 232 | return Promise.reject('implement me'); 233 | } 234 | 235 | async getNetwork(): Promise { 236 | const response = await this.sendRequest( 237 | newRequest(ConsensusEndpoint.NET_INFO) 238 | ); 239 | 240 | return this.parseResponse(response); 241 | } 242 | 243 | async getAccountSequence(address: string, height?: number): Promise { 244 | const response = await this.sendRequest( 245 | newRequest(ABCIEndpoint.ABCI_QUERY, [ 246 | `auth/accounts/${address}`, 247 | '', 248 | '0', // Height; not supported > 0 for now 249 | false, 250 | ]) 251 | ); 252 | 253 | // Parse the response 254 | const abciResponse = this.parseResponse(response); 255 | 256 | return extractSequenceFromResponse(abciResponse.response.ResponseBase.Data); 257 | } 258 | 259 | async getAccountNumber(address: string, height?: number): Promise { 260 | const response = await this.sendRequest( 261 | newRequest(ABCIEndpoint.ABCI_QUERY, [ 262 | `auth/accounts/${address}`, 263 | '', 264 | '0', // Height; not supported > 0 for now 265 | false, 266 | ]) 267 | ); 268 | 269 | // Parse the response 270 | const abciResponse = this.parseResponse(response); 271 | 272 | return extractAccountNumberFromResponse( 273 | abciResponse.response.ResponseBase.Data 274 | ); 275 | } 276 | 277 | async getStatus(): Promise { 278 | const response = await this.sendRequest( 279 | newRequest(CommonEndpoint.STATUS) 280 | ); 281 | 282 | return this.parseResponse(response); 283 | } 284 | 285 | async getTransaction(hash: string): Promise { 286 | const response = await this.sendRequest( 287 | newRequest(TransactionEndpoint.TX, [hash]) 288 | ); 289 | 290 | return this.parseResponse(response); 291 | } 292 | 293 | async sendTransaction( 294 | tx: string, 295 | endpoint: K 296 | ): Promise { 297 | const request: RPCRequest = newRequest(endpoint, [tx]); 298 | 299 | switch (endpoint) { 300 | case TransactionEndpoint.BROADCAST_TX_COMMIT: 301 | // The endpoint is a commit broadcast 302 | // (it waits for the transaction to be committed) to the chain before returning 303 | return this.broadcastTxCommit(request); 304 | case TransactionEndpoint.BROADCAST_TX_SYNC: 305 | default: 306 | return this.broadcastTxSync(request); 307 | } 308 | } 309 | 310 | private async broadcastTxSync( 311 | request: RPCRequest 312 | ): Promise { 313 | const response: RPCResponse = 314 | await this.sendRequest(request); 315 | 316 | const broadcastResponse: BroadcastTxSyncResult = 317 | this.parseResponse(response); 318 | 319 | // Check if there is an immediate tx-broadcast error 320 | // (originating from basic transaction checks like CheckTx) 321 | if (broadcastResponse.error) { 322 | const errType: string = broadcastResponse.error[ABCIErrorKey]; 323 | const log: string = broadcastResponse.Log; 324 | 325 | throw constructRequestError(errType, log); 326 | } 327 | 328 | return broadcastResponse; 329 | } 330 | 331 | private async broadcastTxCommit( 332 | request: RPCRequest 333 | ): Promise { 334 | const response: RPCResponse = 335 | await this.sendRequest(request); 336 | 337 | const broadcastResponse: BroadcastTxCommitResult = 338 | this.parseResponse(response); 339 | 340 | const { check_tx, deliver_tx } = broadcastResponse; 341 | 342 | // Check if there is an immediate tx-broadcast error (in CheckTx) 343 | if (check_tx.ResponseBase.Error) { 344 | const errType: string = check_tx.ResponseBase.Error[ABCIErrorKey]; 345 | const log: string = check_tx.ResponseBase.Log; 346 | 347 | throw constructRequestError(errType, log); 348 | } 349 | 350 | // Check if there is a parsing error with the transaction (in DeliverTx) 351 | if (deliver_tx.ResponseBase.Error) { 352 | const errType: string = deliver_tx.ResponseBase.Error[ABCIErrorKey]; 353 | const log: string = deliver_tx.ResponseBase.Log; 354 | 355 | throw constructRequestError(errType, log); 356 | } 357 | 358 | return broadcastResponse; 359 | } 360 | 361 | waitForTransaction( 362 | hash: string, 363 | fromHeight?: number, 364 | timeout?: number 365 | ): Promise { 366 | return waitForTransaction(this, hash, fromHeight, timeout); 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rest/restService'; 2 | export * from './rest/restService.types'; 3 | -------------------------------------------------------------------------------- /src/services/rest/restService.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { RequestParams } from './restService.types'; 3 | import { RPCResponse } from '../../provider'; 4 | 5 | export class RestService { 6 | static async post( 7 | baseURL: string, 8 | params: RequestParams 9 | ): Promise { 10 | const axiosResponse = await axios.post>( 11 | baseURL, 12 | params.request, 13 | params.config 14 | ); 15 | 16 | const { result, error } = axiosResponse.data; 17 | 18 | // Check for errors 19 | if (error) { 20 | // Error encountered during the POST request 21 | throw new Error(`${error.message}: ${error.data}`); 22 | } 23 | 24 | // Check for the correct result format 25 | if (!result) { 26 | throw new Error('invalid result returned'); 27 | } 28 | 29 | return result; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/services/rest/restService.types.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from 'axios'; 2 | import { RPCRequest } from '../../provider'; 3 | 4 | export interface RequestParams { 5 | request: RPCRequest; 6 | config?: AxiosRequestConfig; 7 | } 8 | -------------------------------------------------------------------------------- /src/wallet/index.ts: -------------------------------------------------------------------------------- 1 | export * from './key'; 2 | export * from './ledger'; 3 | export * from './signer'; 4 | export * from './types'; 5 | export * from './utility'; 6 | export * from './wallet'; 7 | -------------------------------------------------------------------------------- /src/wallet/key/index.ts: -------------------------------------------------------------------------------- 1 | export * from './key'; 2 | -------------------------------------------------------------------------------- /src/wallet/key/key.test.ts: -------------------------------------------------------------------------------- 1 | import { generateEntropy, generateKeyPair, stringToUTF8 } from '../utility'; 2 | import { entropyToMnemonic } from '@cosmjs/crypto/build/bip39'; 3 | import { KeySigner } from './key'; 4 | 5 | describe('Private Key Signer', () => { 6 | const generateRandomKeySigner = async ( 7 | index?: number 8 | ): Promise => { 9 | const { publicKey, privateKey } = await generateKeyPair( 10 | entropyToMnemonic(generateEntropy()), 11 | index ? index : 0 12 | ); 13 | 14 | return new KeySigner(privateKey, publicKey); 15 | }; 16 | 17 | test('getAddress', async () => { 18 | const signer: KeySigner = await generateRandomKeySigner(); 19 | const address: string = await signer.getAddress(); 20 | 21 | expect(address.length).toBe(40); 22 | }); 23 | 24 | test('getPublicKey', async () => { 25 | const signer: KeySigner = await generateRandomKeySigner(); 26 | const publicKey: Uint8Array = await signer.getPublicKey(); 27 | 28 | expect(publicKey).not.toBeNull(); 29 | expect(publicKey).toHaveLength(65); 30 | }); 31 | 32 | test('getPrivateKey', async () => { 33 | const signer: KeySigner = await generateRandomKeySigner(); 34 | const privateKey: Uint8Array = await signer.getPrivateKey(); 35 | 36 | expect(privateKey).not.toBeNull(); 37 | expect(privateKey).toHaveLength(32); 38 | }); 39 | 40 | test('signData', async () => { 41 | const rawData: Uint8Array = stringToUTF8('random raw data'); 42 | const signer: KeySigner = await generateRandomKeySigner(); 43 | 44 | // Sign the data 45 | const signature: Uint8Array = await signer.signData(rawData); 46 | 47 | // Verify the signature 48 | const isValid: boolean = await signer.verifySignature(rawData, signature); 49 | 50 | expect(isValid).toBe(true); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/wallet/key/key.ts: -------------------------------------------------------------------------------- 1 | import { Signer } from '../signer'; 2 | import { encodeSecp256k1Pubkey, pubkeyToAddress } from '@cosmjs/amino'; 3 | import { Secp256k1, Secp256k1Signature, sha256 } from '@cosmjs/crypto'; 4 | import { defaultAddressPrefix } from '../utility'; 5 | 6 | /** 7 | * KeySigner implements the logic for the private key signer 8 | */ 9 | export class KeySigner implements Signer { 10 | private readonly privateKey: Uint8Array; // the raw private key 11 | private readonly publicKey: Uint8Array; // the compressed public key 12 | private readonly addressPrefix: string; // the address prefix 13 | 14 | /** 15 | * Creates a new {@link KeySigner} instance 16 | * @param {Uint8Array} privateKey the raw Secp256k1 private key 17 | * @param {Uint8Array} publicKey the raw Secp256k1 public key 18 | * @param {string} addressPrefix the address prefix 19 | */ 20 | constructor( 21 | privateKey: Uint8Array, 22 | publicKey: Uint8Array, 23 | addressPrefix: string = defaultAddressPrefix 24 | ) { 25 | this.privateKey = privateKey; 26 | this.publicKey = publicKey; 27 | this.addressPrefix = addressPrefix; 28 | } 29 | 30 | getAddress = async (): Promise => { 31 | return pubkeyToAddress( 32 | encodeSecp256k1Pubkey(Secp256k1.compressPubkey(this.publicKey)), 33 | this.addressPrefix 34 | ); 35 | }; 36 | 37 | getPublicKey = async (): Promise => { 38 | return this.publicKey; 39 | }; 40 | 41 | getPrivateKey = async (): Promise => { 42 | return this.privateKey; 43 | }; 44 | 45 | signData = async (data: Uint8Array): Promise => { 46 | const signature = await Secp256k1.createSignature( 47 | sha256(data), 48 | this.privateKey 49 | ); 50 | 51 | return new Uint8Array([ 52 | ...(signature.r(32) as any), 53 | ...(signature.s(32) as any), 54 | ]); 55 | }; 56 | 57 | verifySignature = async ( 58 | data: Uint8Array, 59 | signature: Uint8Array 60 | ): Promise => { 61 | return Secp256k1.verifySignature( 62 | Secp256k1Signature.fromFixedLength(signature), 63 | sha256(data), 64 | this.publicKey 65 | ); 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /src/wallet/ledger/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ledger'; 2 | -------------------------------------------------------------------------------- /src/wallet/ledger/ledger.ts: -------------------------------------------------------------------------------- 1 | import { Signer } from '../signer'; 2 | import { LedgerConnector } from '@cosmjs/ledger-amino'; 3 | import { defaultAddressPrefix, generateHDPath } from '../utility'; 4 | import { HdPath, Secp256k1, Secp256k1Signature, sha256 } from '@cosmjs/crypto'; 5 | import { encodeSecp256k1Pubkey, pubkeyToAddress } from '@cosmjs/amino'; 6 | 7 | /** 8 | * LedgerSigner implements the logic for the Ledger device signer 9 | */ 10 | export class LedgerSigner implements Signer { 11 | private readonly connector: LedgerConnector; 12 | private readonly hdPath: HdPath; 13 | private readonly addressPrefix: string; 14 | 15 | /** 16 | * Creates a new Ledger device signer instance 17 | * @param {LedgerConnector} connector the Ledger connector 18 | * @param {number} accountIndex the desired account index 19 | * @param {string} addressPrefix the address prefix 20 | */ 21 | constructor( 22 | connector: LedgerConnector, 23 | accountIndex: number, 24 | addressPrefix: string = defaultAddressPrefix 25 | ) { 26 | this.connector = connector; 27 | this.hdPath = generateHDPath(accountIndex); 28 | this.addressPrefix = addressPrefix; 29 | } 30 | 31 | getAddress = async (): Promise => { 32 | if (!this.connector) { 33 | throw new Error('Ledger not connected'); 34 | } 35 | 36 | const compressedPubKey: Uint8Array = await this.connector.getPubkey( 37 | this.hdPath 38 | ); 39 | 40 | return pubkeyToAddress( 41 | encodeSecp256k1Pubkey(compressedPubKey), 42 | this.addressPrefix 43 | ); 44 | }; 45 | 46 | getPublicKey = async (): Promise => { 47 | if (!this.connector) { 48 | throw new Error('Ledger not connected'); 49 | } 50 | 51 | return this.connector.getPubkey(this.hdPath); 52 | }; 53 | 54 | getPrivateKey = async (): Promise => { 55 | throw new Error('Ledger does not support private key exports'); 56 | }; 57 | 58 | signData = async (data: Uint8Array): Promise => { 59 | if (!this.connector) { 60 | throw new Error('Ledger not connected'); 61 | } 62 | 63 | return this.connector.sign(data, this.hdPath); 64 | }; 65 | 66 | verifySignature = async ( 67 | data: Uint8Array, 68 | signature: Uint8Array 69 | ): Promise => { 70 | const publicKey = await this.getPublicKey(); 71 | 72 | return Secp256k1.verifySignature( 73 | Secp256k1Signature.fromFixedLength(signature), 74 | sha256(data), 75 | publicKey 76 | ); 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /src/wallet/signer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Signer is the base signer API. 3 | * The signer manages data signing 4 | */ 5 | export interface Signer { 6 | /** 7 | * Returns the address associated with the signer's public key 8 | */ 9 | getAddress(): Promise; 10 | 11 | /** 12 | * Returns the signer's Secp256k1-compressed public key 13 | */ 14 | getPublicKey(): Promise; 15 | 16 | /** 17 | * Returns the signer's actual raw private key 18 | */ 19 | getPrivateKey(): Promise; 20 | 21 | /** 22 | * Generates a data signature for arbitrary input 23 | * @param {Uint8Array} data the data to be signed 24 | */ 25 | signData(data: Uint8Array): Promise; 26 | 27 | /** 28 | * Verifies if the signature matches the provided raw data 29 | * @param {Uint8Array} data the raw data (not-hashed) 30 | * @param {Uint8Array} signature the hashed-data signature 31 | */ 32 | verifySignature(data: Uint8Array, signature: Uint8Array): Promise; 33 | } 34 | -------------------------------------------------------------------------------- /src/wallet/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sign'; 2 | export * from './wallet'; 3 | -------------------------------------------------------------------------------- /src/wallet/types/sign.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The transaction payload that is signed to generate 3 | * a valid transaction signature 4 | */ 5 | export interface TxSignPayload { 6 | // the ID of the chain 7 | chain_id: string; 8 | // the account number of the 9 | // account that's signing (decimal) 10 | account_number: string; 11 | // the sequence number of the 12 | // account that's signing (decimal) 13 | sequence: string; 14 | // the fee of the transaction 15 | fee: { 16 | // gas price of the transaction 17 | // in the format 18 | gas_fee: string; 19 | // gas limit of the transaction (decimal) 20 | gas_wanted: string; 21 | }; 22 | // the messages associated 23 | // with the transaction. 24 | // These messages have the form: \ 25 | // @type: ... 26 | // value: ... 27 | msgs: any[]; 28 | // the transaction memo 29 | memo: string; 30 | } 31 | 32 | export const Secp256k1PubKeyType = '/tm.PubKeySecp256k1'; 33 | -------------------------------------------------------------------------------- /src/wallet/types/wallet.ts: -------------------------------------------------------------------------------- 1 | export interface CreateWalletOptions { 2 | // the address prefix 3 | addressPrefix?: string; 4 | // the requested account index 5 | accountIndex?: number; 6 | } 7 | 8 | export type AccountWalletOption = Pick; 9 | -------------------------------------------------------------------------------- /src/wallet/utility/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utility'; 2 | -------------------------------------------------------------------------------- /src/wallet/utility/utility.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Bip39, 3 | EnglishMnemonic, 4 | HdPath, 5 | Secp256k1, 6 | Slip10, 7 | Slip10Curve, 8 | Slip10RawIndex, 9 | } from '@cosmjs/crypto'; 10 | import crypto from 'crypto'; 11 | 12 | /** 13 | * Generates the HD path, for the specified index, in the form 'm/44'/118'/0'/0/i', 14 | * where 'i' is the account index 15 | * @param {number} [index=0] the account index 16 | */ 17 | export const generateHDPath = (index?: number): HdPath => { 18 | return [ 19 | Slip10RawIndex.hardened(44), 20 | Slip10RawIndex.hardened(118), 21 | Slip10RawIndex.hardened(0), 22 | Slip10RawIndex.normal(0), 23 | Slip10RawIndex.normal(index ? index : 0), 24 | ]; 25 | }; 26 | 27 | /** 28 | * Generates random entropy of the specified size (in B) 29 | * @param {number} [size=32] the entropy size in bytes 30 | */ 31 | export const generateEntropy = (size?: number): Uint8Array => { 32 | const array = new Uint8Array(size ? size : 32); 33 | 34 | // Generate random data 35 | crypto.randomFillSync(array); 36 | 37 | return array; 38 | }; 39 | 40 | interface keyPair { 41 | privateKey: Uint8Array; 42 | publicKey: Uint8Array; 43 | } 44 | 45 | /** 46 | * Generates a new Secp256k1 key-pair using 47 | * the provided English mnemonic and account index 48 | * @param {string} mnemonic the English mnemonic 49 | * @param {number} [accountIndex=0] the account index 50 | */ 51 | export const generateKeyPair = async ( 52 | mnemonic: string, 53 | accountIndex?: number 54 | ): Promise => { 55 | // Generate the seed 56 | const seed = await Bip39.mnemonicToSeed(new EnglishMnemonic(mnemonic)); 57 | 58 | // Derive the private key 59 | const { privkey: privateKey } = Slip10.derivePath( 60 | Slip10Curve.Secp256k1, 61 | seed, 62 | generateHDPath(accountIndex) 63 | ); 64 | 65 | // Derive the public key 66 | const { pubkey: publicKey } = await Secp256k1.makeKeypair(privateKey); 67 | 68 | return { 69 | publicKey: publicKey, 70 | privateKey: privateKey, 71 | }; 72 | }; 73 | 74 | // Address prefix for TM2 networks 75 | export const defaultAddressPrefix = 'g'; 76 | 77 | /** 78 | * Encodes a string into a Uint8Array 79 | * @param {string} str the string to be encoded 80 | */ 81 | export const stringToUTF8 = (str: string): Uint8Array => { 82 | return new TextEncoder().encode(str); 83 | }; 84 | 85 | /** 86 | * Escapes <,>,& in string. 87 | * Golang's json marshaller escapes <,>,& by default. 88 | * https://cs.opensource.google/go/go/+/refs/tags/go1.20.6:src/encoding/json/encode.go;l=46-53 89 | */ 90 | export function encodeCharacterSet(data: string) { 91 | return data 92 | .replace(//g, '\\u003e') 94 | .replace(/&/g, '\\u0026'); 95 | } 96 | -------------------------------------------------------------------------------- /src/wallet/wallet.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BroadcastTxSyncResult, 3 | JSONRPCProvider, 4 | Status, 5 | TransactionEndpoint, 6 | } from '../provider'; 7 | import { mock } from 'jest-mock-extended'; 8 | import { Wallet } from './wallet'; 9 | import { EnglishMnemonic, Secp256k1 } from '@cosmjs/crypto'; 10 | import { 11 | defaultAddressPrefix, 12 | generateEntropy, 13 | generateKeyPair, 14 | } from './utility'; 15 | import { entropyToMnemonic } from '@cosmjs/crypto/build/bip39'; 16 | import { KeySigner } from './key'; 17 | import { Signer } from './signer'; 18 | import { Tx, TxSignature } from '../proto'; 19 | import Long from 'long'; 20 | import { Secp256k1PubKeyType } from './types'; 21 | import { Any } from '../proto/google/protobuf/any'; 22 | 23 | describe('Wallet', () => { 24 | test('createRandom', async () => { 25 | const wallet: Wallet = await Wallet.createRandom(); 26 | 27 | expect(wallet).not.toBeNull(); 28 | 29 | const address: string = await wallet.getAddress(); 30 | 31 | expect(address).toHaveLength(40); 32 | }); 33 | 34 | test('connect', async () => { 35 | const mockProvider = mock(); 36 | const wallet: Wallet = await Wallet.createRandom(); 37 | 38 | // Connect the provider 39 | wallet.connect(mockProvider); 40 | 41 | expect(wallet.getProvider()).toBe(mockProvider); 42 | }); 43 | 44 | test('fromMnemonic', async () => { 45 | const mnemonic: EnglishMnemonic = new EnglishMnemonic( 46 | 'lens balcony basic cherry half purchase balance soccer solar scissors process eager orchard fatigue rural retire approve crouch repair prepare develop clarify milk suffer' 47 | ); 48 | const wallet: Wallet = await Wallet.fromMnemonic(mnemonic.toString()); 49 | 50 | expect(wallet).not.toBeNull(); 51 | 52 | // Fetch the address 53 | const address: string = await wallet.getAddress(); 54 | 55 | expect(address).toBe( 56 | `${defaultAddressPrefix}1vcjvkjdvckprkcpm7l44plrtg83asfu9geaz90` 57 | ); 58 | }); 59 | 60 | test('fromPrivateKey', async () => { 61 | const { publicKey, privateKey } = await generateKeyPair( 62 | entropyToMnemonic(generateEntropy()), 63 | 0 64 | ); 65 | const signer: Signer = new KeySigner( 66 | privateKey, 67 | Secp256k1.compressPubkey(publicKey) 68 | ); 69 | 70 | const wallet: Wallet = await Wallet.fromPrivateKey(privateKey); 71 | const walletSigner: Signer = wallet.getSigner(); 72 | 73 | expect(wallet).not.toBeNull(); 74 | expect(await wallet.getAddress()).toBe(await signer.getAddress()); 75 | expect(await walletSigner.getPublicKey()).toEqual( 76 | await signer.getPublicKey() 77 | ); 78 | }); 79 | 80 | test('getAccountSequence', async () => { 81 | const mockSequence = 5; 82 | const mockProvider = mock(); 83 | mockProvider.getAccountSequence.mockResolvedValue(mockSequence); 84 | 85 | const wallet: Wallet = await Wallet.createRandom(); 86 | wallet.connect(mockProvider); 87 | 88 | const address: string = await wallet.getAddress(); 89 | const sequence: number = await wallet.getAccountSequence(); 90 | 91 | expect(mockProvider.getAccountSequence).toHaveBeenCalledWith( 92 | address, 93 | undefined 94 | ); 95 | expect(sequence).toBe(mockSequence); 96 | }); 97 | 98 | test('getAccountNumber', async () => { 99 | const mockAccountNumber = 10; 100 | const mockProvider = mock(); 101 | mockProvider.getAccountNumber.mockResolvedValue(mockAccountNumber); 102 | 103 | const wallet: Wallet = await Wallet.createRandom(); 104 | wallet.connect(mockProvider); 105 | 106 | const address: string = await wallet.getAddress(); 107 | const accountNumber: number = await wallet.getAccountNumber(); 108 | 109 | expect(mockProvider.getAccountNumber).toHaveBeenCalledWith( 110 | address, 111 | undefined 112 | ); 113 | expect(accountNumber).toBe(mockAccountNumber); 114 | }); 115 | 116 | test('getBalance', async () => { 117 | const mockBalance = 100; 118 | const mockProvider = mock(); 119 | mockProvider.getBalance.mockResolvedValue(mockBalance); 120 | 121 | const wallet: Wallet = await Wallet.createRandom(); 122 | wallet.connect(mockProvider); 123 | 124 | const address: string = await wallet.getAddress(); 125 | const balance: number = await wallet.getBalance(); 126 | 127 | expect(mockProvider.getBalance).toHaveBeenCalledWith(address, 'ugnot'); 128 | expect(balance).toBe(mockBalance); 129 | }); 130 | 131 | test('getGasPrice', async () => { 132 | const mockGasPrice = 1000; 133 | const mockProvider = mock(); 134 | mockProvider.getGasPrice.mockResolvedValue(mockGasPrice); 135 | 136 | const wallet: Wallet = await Wallet.createRandom(); 137 | wallet.connect(mockProvider); 138 | 139 | const gasPrice: number = await wallet.getGasPrice(); 140 | 141 | expect(mockProvider.getGasPrice).toHaveBeenCalled(); 142 | expect(gasPrice).toBe(mockGasPrice); 143 | }); 144 | 145 | test('estimateGas', async () => { 146 | const mockTxEstimation = 1000; 147 | const mockTx = mock(); 148 | const mockProvider = mock(); 149 | mockProvider.estimateGas.mockResolvedValue(mockTxEstimation); 150 | 151 | const wallet: Wallet = await Wallet.createRandom(); 152 | wallet.connect(mockProvider); 153 | 154 | const estimation: number = await wallet.estimateGas(mockTx); 155 | 156 | expect(mockProvider.estimateGas).toHaveBeenCalledWith(mockTx); 157 | expect(estimation).toBe(mockTxEstimation); 158 | }); 159 | 160 | test('signTransaction', async () => { 161 | const mockTx = mock(); 162 | mockTx.signatures = []; 163 | mockTx.fee = { 164 | gasFee: '10', 165 | gasWanted: new Long(10), 166 | }; 167 | mockTx.messages = []; 168 | 169 | const mockStatus = mock(); 170 | mockStatus.node_info = { 171 | version_set: [], 172 | version: '', 173 | net_address: '', 174 | software: '', 175 | channels: '', 176 | monkier: '', 177 | other: { 178 | tx_index: '', 179 | rpc_address: '', 180 | }, 181 | network: 'testchain', 182 | }; 183 | 184 | const mockProvider = mock(); 185 | mockProvider.getStatus.mockResolvedValue(mockStatus); 186 | mockProvider.getAccountNumber.mockResolvedValue(10); 187 | mockProvider.getAccountSequence.mockResolvedValue(10); 188 | 189 | const wallet: Wallet = await Wallet.createRandom(); 190 | wallet.connect(mockProvider); 191 | 192 | const emptyDecodeCallback = (_: Any[]): any[] => { 193 | return []; 194 | }; 195 | const signedTx: Tx = await wallet.signTransaction( 196 | mockTx, 197 | emptyDecodeCallback 198 | ); 199 | 200 | expect(mockProvider.getStatus).toHaveBeenCalled(); 201 | expect(mockProvider.getAccountNumber).toHaveBeenCalled(); 202 | expect(mockProvider.getAccountSequence).toHaveBeenCalled(); 203 | 204 | expect(signedTx.signatures).toHaveLength(1); 205 | 206 | const sig: TxSignature = signedTx.signatures[0]; 207 | expect(sig.pubKey?.typeUrl).toBe(Secp256k1PubKeyType); 208 | expect(sig.pubKey?.value).not.toBeNull(); 209 | expect(sig.signature).not.toBeNull(); 210 | }); 211 | 212 | test('sendTransaction', async () => { 213 | const mockTx = mock(); 214 | mockTx.signatures = []; 215 | mockTx.fee = { 216 | gasFee: '10', 217 | gasWanted: new Long(10), 218 | }; 219 | mockTx.messages = []; 220 | mockTx.memo = ''; 221 | 222 | const mockTxHash = 'tx hash'; 223 | 224 | const mockStatus = mock(); 225 | mockStatus.node_info = { 226 | version_set: [], 227 | version: '', 228 | net_address: '', 229 | software: '', 230 | channels: '', 231 | monkier: '', 232 | other: { 233 | tx_index: '', 234 | rpc_address: '', 235 | }, 236 | network: 'testchain', 237 | }; 238 | 239 | const mockTransaction: BroadcastTxSyncResult = { 240 | error: null, 241 | data: null, 242 | Log: '', 243 | hash: mockTxHash, 244 | }; 245 | 246 | const mockProvider = mock(); 247 | mockProvider.getStatus.mockResolvedValue(mockStatus); 248 | mockProvider.getAccountNumber.mockResolvedValue(10); 249 | mockProvider.getAccountSequence.mockResolvedValue(10); 250 | mockProvider.sendTransaction.mockResolvedValue(mockTransaction); 251 | 252 | const wallet: Wallet = await Wallet.createRandom(); 253 | wallet.connect(mockProvider); 254 | 255 | const tx: BroadcastTxSyncResult = await wallet.sendTransaction( 256 | mockTx, 257 | TransactionEndpoint.BROADCAST_TX_SYNC 258 | ); 259 | 260 | expect(tx.hash).toBe(mockTxHash); 261 | }); 262 | }); 263 | -------------------------------------------------------------------------------- /src/wallet/wallet.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BroadcastTransactionMap, 3 | Provider, 4 | Status, 5 | uint8ArrayToBase64, 6 | } from '../provider'; 7 | import { Signer } from './signer'; 8 | import { LedgerSigner } from './ledger'; 9 | import { KeySigner } from './key'; 10 | import { Secp256k1 } from '@cosmjs/crypto'; 11 | import { 12 | encodeCharacterSet, 13 | generateEntropy, 14 | generateKeyPair, 15 | stringToUTF8, 16 | } from './utility'; 17 | import { LedgerConnector } from '@cosmjs/ledger-amino'; 18 | import { entropyToMnemonic } from '@cosmjs/crypto/build/bip39'; 19 | import { Any, PubKeySecp256k1, Tx, TxSignature } from '../proto'; 20 | import { 21 | AccountWalletOption, 22 | CreateWalletOptions, 23 | Secp256k1PubKeyType, 24 | TxSignPayload, 25 | } from './types'; 26 | import { sortedJsonStringify } from '@cosmjs/amino/build/signdoc'; 27 | 28 | /** 29 | * Wallet is a single account abstraction 30 | * that can interact with the blockchain 31 | */ 32 | export class Wallet { 33 | protected provider: Provider; 34 | protected signer: Signer; 35 | 36 | /** 37 | * Connects the wallet to the specified {@link Provider} 38 | * @param {Provider} provider the active {@link Provider}, if any 39 | */ 40 | connect = (provider: Provider) => { 41 | this.provider = provider; 42 | }; 43 | 44 | // Wallet initialization // 45 | 46 | /** 47 | * Generates a private key-based wallet, using a random seed 48 | * @param {AccountWalletOption} options the account options 49 | */ 50 | static createRandom = async ( 51 | options?: AccountWalletOption 52 | ): Promise => { 53 | const { publicKey, privateKey } = await generateKeyPair( 54 | entropyToMnemonic(generateEntropy()), 55 | 0 56 | ); 57 | 58 | // Initialize the wallet 59 | const wallet: Wallet = new Wallet(); 60 | wallet.signer = new KeySigner( 61 | privateKey, 62 | Secp256k1.compressPubkey(publicKey), 63 | options?.addressPrefix 64 | ); 65 | 66 | return wallet; 67 | }; 68 | 69 | /** 70 | * Generates a custom signer-based wallet 71 | * @param {Signer} signer the custom signer implementing the Signer interface 72 | * @param {CreateWalletOptions} options the wallet generation options 73 | */ 74 | static fromSigner = async (signer: Signer): Promise => { 75 | // Initialize the wallet 76 | const wallet: Wallet = new Wallet(); 77 | wallet.signer = signer; 78 | 79 | return wallet; 80 | }; 81 | 82 | /** 83 | * Generates a bip39 mnemonic-based wallet 84 | * @param {string} mnemonic the bip39 mnemonic 85 | * @param {CreateWalletOptions} options the wallet generation options 86 | */ 87 | static fromMnemonic = async ( 88 | mnemonic: string, 89 | options?: CreateWalletOptions 90 | ): Promise => { 91 | const { publicKey, privateKey } = await generateKeyPair( 92 | mnemonic, 93 | options?.accountIndex 94 | ); 95 | 96 | // Initialize the wallet 97 | const wallet: Wallet = new Wallet(); 98 | wallet.signer = new KeySigner( 99 | privateKey, 100 | Secp256k1.compressPubkey(publicKey), 101 | options?.addressPrefix 102 | ); 103 | 104 | return wallet; 105 | }; 106 | 107 | /** 108 | * Generates a private key-based wallet 109 | * @param {string} privateKey the private key 110 | * @param {AccountWalletOption} options the account options 111 | */ 112 | static fromPrivateKey = async ( 113 | privateKey: Uint8Array, 114 | options?: AccountWalletOption 115 | ): Promise => { 116 | // Derive the public key 117 | const { pubkey: publicKey } = await Secp256k1.makeKeypair(privateKey); 118 | 119 | // Initialize the wallet 120 | const wallet: Wallet = new Wallet(); 121 | wallet.signer = new KeySigner( 122 | privateKey, 123 | Secp256k1.compressPubkey(publicKey), 124 | options?.addressPrefix 125 | ); 126 | 127 | return wallet; 128 | }; 129 | 130 | /** 131 | * Creates a Ledger-based wallet 132 | * @param {LedgerConnector} connector the Ledger device connector 133 | * @param {CreateWalletOptions} options the wallet generation options 134 | */ 135 | static fromLedger = ( 136 | connector: LedgerConnector, 137 | options?: CreateWalletOptions 138 | ): Wallet => { 139 | const wallet: Wallet = new Wallet(); 140 | 141 | wallet.signer = new LedgerSigner( 142 | connector, 143 | options?.accountIndex ?? 0, 144 | options?.addressPrefix 145 | ); 146 | 147 | return wallet; 148 | }; 149 | 150 | // Account manipulation // 151 | 152 | /** 153 | * Fetches the address associated with the wallet 154 | */ 155 | getAddress = (): Promise => { 156 | return this.signer.getAddress(); 157 | }; 158 | 159 | /** 160 | * Fetches the account sequence for the wallet 161 | * @param {number} [height=latest] the block height 162 | */ 163 | getAccountSequence = async (height?: number): Promise => { 164 | if (!this.provider) { 165 | throw new Error('provider not connected'); 166 | } 167 | 168 | // Get the address 169 | const address: string = await this.getAddress(); 170 | 171 | return this.provider.getAccountSequence(address, height); 172 | }; 173 | 174 | /** 175 | * Fetches the account number for the wallet. Errors out if the 176 | * account is not initialized 177 | * @param {number} [height=latest] the block height 178 | */ 179 | getAccountNumber = async (height?: number): Promise => { 180 | if (!this.provider) { 181 | throw new Error('provider not connected'); 182 | } 183 | 184 | // Get the address 185 | const address: string = await this.getAddress(); 186 | 187 | return this.provider.getAccountNumber(address, height); 188 | }; 189 | 190 | /** 191 | * Fetches the account balance for the specific denomination 192 | * @param {string} [denomination=ugnot] the fund denomination 193 | */ 194 | getBalance = async (denomination?: string): Promise => { 195 | if (!this.provider) { 196 | throw new Error('provider not connected'); 197 | } 198 | 199 | // Get the address 200 | const address: string = await this.getAddress(); 201 | 202 | return this.provider.getBalance( 203 | address, 204 | denomination ? denomination : 'ugnot' 205 | ); 206 | }; 207 | 208 | /** 209 | * Fetches the current (recommended) average gas price 210 | */ 211 | getGasPrice = async (): Promise => { 212 | if (!this.provider) { 213 | throw new Error('provider not connected'); 214 | } 215 | 216 | return this.provider.getGasPrice(); 217 | }; 218 | 219 | /** 220 | * Estimates the gas limit for the transaction 221 | * @param {Tx} tx the transaction that needs estimating 222 | */ 223 | estimateGas = async (tx: Tx): Promise => { 224 | if (!this.provider) { 225 | throw new Error('provider not connected'); 226 | } 227 | 228 | return this.provider.estimateGas(tx); 229 | }; 230 | 231 | /** 232 | * Returns the connected provider, if any 233 | */ 234 | getProvider = (): Provider => { 235 | return this.provider; 236 | }; 237 | 238 | /** 239 | * Generates a transaction signature, and appends it to the transaction 240 | * @param {Tx} tx the transaction to be signed 241 | * @param {(messages: Any[]) => any[]} decodeTxMessages tx message decode callback 242 | * that should expand the concrete message fields into an object. Required because 243 | * the transaction sign bytes are generated using sorted JSON, which requires 244 | * encoded message values to be decoded for sorting 245 | */ 246 | signTransaction = async ( 247 | tx: Tx, 248 | decodeTxMessages: (messages: Any[]) => any[] 249 | ): Promise => { 250 | if (!this.provider) { 251 | throw new Error('provider not connected'); 252 | } 253 | 254 | // Make sure the tx fee is initialized 255 | if (!tx.fee) { 256 | throw new Error('invalid transaction fee provided'); 257 | } 258 | 259 | // Extract the relevant chain data 260 | const status: Status = await this.provider.getStatus(); 261 | const chainID: string = status.node_info.network; 262 | 263 | // Extract the relevant account data 264 | const address: string = await this.getAddress(); 265 | const accountNumber: number = await this.provider.getAccountNumber(address); 266 | const accountSequence: number = 267 | await this.provider.getAccountSequence(address); 268 | const publicKey: Uint8Array = await this.signer.getPublicKey(); 269 | 270 | // Create the signature payload 271 | const signPayload: TxSignPayload = { 272 | chain_id: chainID, 273 | account_number: accountNumber.toString(10), 274 | sequence: accountSequence.toString(10), 275 | fee: { 276 | gas_fee: tx.fee.gasFee, 277 | gas_wanted: tx.fee.gasWanted.toString(10), 278 | }, 279 | msgs: decodeTxMessages(tx.messages), // unrolled message objects 280 | memo: tx.memo, 281 | }; 282 | 283 | // The TM2 node does signature verification using 284 | // a sorted JSON object, so the payload needs to be sorted 285 | // before signing 286 | const signBytes: Uint8Array = stringToUTF8( 287 | encodeCharacterSet(sortedJsonStringify(signPayload)) 288 | ); 289 | 290 | // The public key needs to be encoded using protobuf for Amino 291 | const wrappedKey: PubKeySecp256k1 = { 292 | key: publicKey, 293 | }; 294 | 295 | // Generate the signature 296 | const txSignature: TxSignature = { 297 | pubKey: { 298 | typeUrl: Secp256k1PubKeyType, 299 | value: PubKeySecp256k1.encode(wrappedKey).finish(), 300 | }, 301 | signature: await this.getSigner().signData(signBytes), 302 | }; 303 | 304 | // Append the signature 305 | return { 306 | ...tx, 307 | signatures: [...tx.signatures, txSignature], 308 | }; 309 | }; 310 | 311 | /** 312 | * Encodes and sends the transaction. If the type of endpoint 313 | * is a broadcast commit, waits for the transaction to be committed to the chain. 314 | * The transaction needs to be signed beforehand. 315 | * Returns the transaction hash (base-64) 316 | * @param {Tx} tx the signed transaction 317 | * @param {BroadcastType} endpoint the transaction broadcast type (sync / commit) 318 | */ 319 | async sendTransaction( 320 | tx: Tx, 321 | endpoint: K 322 | ): Promise { 323 | if (!this.provider) { 324 | throw new Error('provider not connected'); 325 | } 326 | 327 | // Encode the transaction to base-64 328 | const encodedTx: string = uint8ArrayToBase64(Tx.encode(tx).finish()); 329 | 330 | // Send the encoded transaction 331 | return this.provider.sendTransaction(encodedTx, endpoint); 332 | } 333 | 334 | /** 335 | * Returns the associated signer 336 | */ 337 | getSigner = (): Signer => { 338 | return this.signer; 339 | }; 340 | } 341 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "./bin", 7 | "rootDir": "./src", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "declaration": true, 13 | "strictPropertyInitialization": false 14 | }, 15 | "include": ["./src/**/*.ts"], 16 | "exclude": ["./node_modules"] 17 | } 18 | --------------------------------------------------------------------------------