├── .prettierignore ├── src ├── wallet │ ├── key │ │ ├── index.ts │ │ ├── key.test.ts │ │ └── key.ts │ ├── ledger │ │ ├── index.ts │ │ └── ledger.ts │ ├── utility │ │ ├── index.ts │ │ └── utility.ts │ ├── types │ │ ├── index.ts │ │ ├── wallet.ts │ │ └── sign.ts │ ├── index.ts │ ├── signer.ts │ ├── wallet.test.ts │ └── wallet.ts ├── provider │ ├── errors │ │ ├── index.ts │ │ ├── messages.ts │ │ └── errors.ts │ ├── websocket │ │ ├── index.ts │ │ ├── ws.ts │ │ └── ws.test.ts │ ├── jsonrpc │ │ ├── index.ts │ │ ├── jsonrpc.ts │ │ └── jsonrpc.test.ts │ ├── utility │ │ ├── index.ts │ │ ├── errors.utility.ts │ │ ├── requests.utility.ts │ │ └── provider.utility.ts │ ├── types │ │ ├── index.ts │ │ ├── jsonrpc.ts │ │ ├── abci.ts │ │ └── common.ts │ ├── index.ts │ ├── endpoints.ts │ └── provider.ts ├── proto │ ├── index.ts │ ├── tm2 │ │ ├── multisig.test.ts │ │ ├── abci.ts │ │ ├── multisig.ts │ │ └── tx.ts │ └── google │ │ └── protobuf │ │ └── any.ts ├── services │ ├── index.ts │ └── rest │ │ ├── restService.types.ts │ │ └── restService.ts └── index.ts ├── .yarnrc.yml ├── jest.config.json ├── .prettierrc ├── .gitignore ├── .github ├── CODEOWNERS ├── workflows │ ├── main.yaml │ ├── test.yaml │ └── lint.yaml └── dependabot.yaml ├── tsconfig.json ├── scripts └── generate.sh ├── proto └── tm2 │ ├── abci.proto │ ├── multisig.proto │ └── tx.proto ├── eslint.config.mjs ├── package.json ├── README.md └── LICENSE.md /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bin 3 | yarn.lock -------------------------------------------------------------------------------- /src/wallet/key/index.ts: -------------------------------------------------------------------------------- 1 | export * from './key'; 2 | -------------------------------------------------------------------------------- /src/provider/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors'; 2 | -------------------------------------------------------------------------------- /src/provider/websocket/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ws'; 2 | -------------------------------------------------------------------------------- /src/wallet/ledger/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ledger'; 2 | -------------------------------------------------------------------------------- /src/wallet/utility/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utility'; 2 | -------------------------------------------------------------------------------- /src/provider/jsonrpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jsonrpc'; 2 | -------------------------------------------------------------------------------- /src/wallet/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sign'; 2 | export * from './wallet'; 3 | -------------------------------------------------------------------------------- /src/proto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tm2/tx'; 2 | export { Any } from './google/protobuf/any'; 3 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rest/restService'; 2 | export * from './rest/restService.types'; 3 | -------------------------------------------------------------------------------- /src/provider/utility/index.ts: -------------------------------------------------------------------------------- 1 | export * from './provider.utility'; 2 | export * from './requests.utility'; 3 | -------------------------------------------------------------------------------- /src/provider/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './abci'; 2 | export * from './common'; 3 | export * from './jsonrpc'; 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './provider'; 2 | export * from './wallet'; 3 | export * from './proto'; 4 | export * from './services'; 5 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testEnvironment": "node", 4 | "modulePathIgnorePatterns": ["bin", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "bracketSpacing": true, 6 | "singleQuote": true 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/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/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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Build files 5 | bin 6 | 7 | # Dev environment metadata 8 | .idea 9 | .DS_Store 10 | *.log 11 | 12 | # Yarn leftovers 13 | .yarn/cache 14 | .pnp.* 15 | .yarn/install-state.gz 16 | .yarn/unplugged -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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=false,emitDefaultValues=json-methods" \ 16 | ${x} 17 | done 18 | -------------------------------------------------------------------------------- /.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@v5 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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /proto/tm2/multisig.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package tm; 3 | 4 | option go_package = "github.com/gnolang/gno/tm2/pkg/crypto/multisig/pb"; 5 | 6 | // imports 7 | import "google/protobuf/any.proto"; 8 | 9 | // messages 10 | message PubKeyMultisig { 11 | uint64 k = 1 [json_name = "threshold"]; 12 | repeated google.protobuf.Any pub_keys = 2 [json_name = "pubkeys"]; 13 | } 14 | 15 | message Multisignature { 16 | CompactBitArray bit_array = 1; 17 | repeated bytes sigs = 2; 18 | } 19 | 20 | message CompactBitArray { 21 | uint32 extra_bits_stored = 1 [json_name = "extra_bits"]; // The number of extra bits in elems. 22 | bytes elems = 2 [json_name = "bits"]; 23 | } -------------------------------------------------------------------------------- /.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@v5 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/proto/tm2/multisig.test.ts: -------------------------------------------------------------------------------- 1 | import { bytesToHex } from '@noble/hashes/utils'; 2 | import { CompactBitArray } from './multisig'; 3 | 4 | describe('TestMarshalCompactBitArrayAmino', () => { 5 | const testCases = [ 6 | { marshalledBA: `null`, hexOutput: '' }, 7 | { marshalledBA: `null`, hexOutput: '' }, 8 | { marshalledBA: `"_"`, hexOutput: '0801120100' }, 9 | { marshalledBA: `"x"`, hexOutput: '0801120180' }, 10 | { marshalledBA: `"xx___"`, hexOutput: '08051201c0' }, 11 | { marshalledBA: `"xx______x"`, hexOutput: '08011202c080' }, 12 | { marshalledBA: `"xx_____________x"`, hexOutput: '1202c001' }, 13 | ]; 14 | 15 | test.each(testCases)('$marshalledBA', async ({ marshalledBA, hexOutput }) => { 16 | // Parse JSON into CompactBitArray 17 | const jsonData = JSON.parse(marshalledBA); 18 | const bA = CompactBitArray.fromJSON(jsonData); 19 | 20 | // Marshal using Amino 21 | const bz = CompactBitArray.encode(bA).finish(); 22 | 23 | // Convert bytes to hex and compare 24 | const actualHex = bytesToHex(bz); 25 | expect(actualHex).toBe(hexOutput); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gnolang/tm2-js-client", 3 | "version": "1.3.3", 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 | "publishConfig": { 24 | "access": "public", 25 | "registry": "https://registry.npmjs.org/" 26 | }, 27 | "devDependencies": { 28 | "@eslint/js": "^9.33.0", 29 | "@types/jest": "^30.0.0", 30 | "@types/long": "^5.0.0", 31 | "@types/node": "^24.3.0", 32 | "@types/ws": "^8.5.11", 33 | "@typescript-eslint/eslint-plugin": "^8.42.0", 34 | "@typescript-eslint/parser": "^8.42.0", 35 | "@typescript-eslint/typescript-estree": "^8.42.0", 36 | "eslint": "^9.35.0", 37 | "eslint-config-prettier": "^10.1.5", 38 | "eslint-plugin-prettier": "5.5.1", 39 | "globals": "^16.3.0", 40 | "jest": "^30.0.4", 41 | "jest-mock-extended": "^4.0.0", 42 | "jest-websocket-mock": "^2.4.0", 43 | "prettier": "^3.6.2", 44 | "ts-jest": "^29.4.0", 45 | "ts-proto": "^2.7.5", 46 | "typescript": "^5.9.2", 47 | "typescript-eslint": "^8.39.1" 48 | }, 49 | "dependencies": { 50 | "@cosmjs/amino": "^0.36.0", 51 | "@cosmjs/crypto": "^0.36.0", 52 | "@cosmjs/ledger-amino": "^0.36.0", 53 | "@types/uuid": "^10.0.0", 54 | "axios": "^1.11.0", 55 | "long": "^5.3.2", 56 | "protobufjs": "^7.5.3", 57 | "uuid": "^11.1.0", 58 | "ws": "^8.18.0" 59 | }, 60 | "scripts": { 61 | "tsc": "tsc", 62 | "lint": "eslint '**/*.ts' --fix", 63 | "prettier": "prettier --write .", 64 | "build": "yarn tsc", 65 | "test": "jest", 66 | "prepare": "yarn build", 67 | "prepublishOnly": "yarn lint && yarn prettier" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ABCIAccount, 3 | BlockInfo, 4 | BlockResult, 5 | BroadcastAsGeneric, 6 | BroadcastTransactionMap, 7 | ConsensusParams, 8 | NetworkInfo, 9 | Status, 10 | } from './types'; 11 | import { Tx } from '../proto'; 12 | 13 | /** 14 | * Read-only abstraction for accessing blockchain data 15 | */ 16 | export interface Provider { 17 | // Account-specific methods // 18 | 19 | /** 20 | * Fetches the denomination balance of the account 21 | * @param {string} address the bech32 address of the account 22 | * @param {string} [denomination=ugnot] the balance denomination 23 | * @param {number} [height=0] the height for querying. 24 | * If omitted, the latest height is used 25 | */ 26 | getBalance( 27 | address: string, 28 | denomination?: string, 29 | height?: number 30 | ): Promise; 31 | 32 | /** 33 | * Fetches the account sequence 34 | * @param {string} address the bech32 address of the account 35 | * @param {number} [height=0] the height for querying. 36 | * If omitted, the latest height is used. 37 | * @deprecated use {@link getAccount} instead 38 | */ 39 | getAccountSequence(address: string, height?: number): Promise; 40 | 41 | /** 42 | * Fetches the account number. Errors out if the account 43 | * is not initialized 44 | * @param {string} address the bech32 address of the account 45 | * @param {number} [height=0] the height for querying. 46 | * If omitted, the latest height is used 47 | * @deprecated use {@link getAccount} instead 48 | */ 49 | getAccountNumber(address: string, height?: number): Promise; 50 | 51 | /** 52 | * Fetches the account. Errors out if the account 53 | * is not initialized 54 | * @param {string} address the bech32 address of the account 55 | * @param {number} [height=0] the height for querying. 56 | * If omitted, the latest height is used 57 | */ 58 | getAccount(address: string, height?: number): Promise; 59 | 60 | /** 61 | * Fetches the block at the specific height, if any 62 | * @param {number} height the height for querying 63 | */ 64 | getBlock(height: number): Promise; 65 | 66 | /** 67 | * Fetches the block at the specific height, if any 68 | * @param {number} height the height for querying 69 | */ 70 | getBlockResult(height: number): Promise; 71 | 72 | /** 73 | * Fetches the latest block number from the chain 74 | */ 75 | getBlockNumber(): Promise; 76 | 77 | // Network-specific methods // 78 | 79 | /** 80 | * Fetches the network information 81 | */ 82 | getNetwork(): Promise; 83 | 84 | /** 85 | * Fetches the consensus params for the specific block height 86 | * @param {number} height the height for querying 87 | */ 88 | getConsensusParams(height: number): Promise; 89 | 90 | /** 91 | * Fetches the current node status 92 | */ 93 | getStatus(): Promise; 94 | 95 | /** 96 | * Fetches the current (recommended) average gas price 97 | */ 98 | getGasPrice(): Promise; 99 | 100 | /** 101 | * Estimates the gas limit for the transaction 102 | * @param {Tx} tx the transaction that needs estimating 103 | */ 104 | estimateGas(tx: Tx): Promise; 105 | 106 | // Transaction specific methods // 107 | 108 | /** 109 | * Sends the transaction to the node. If the type of endpoint 110 | * is a broadcast commit, waits for the transaction to be committed to the chain. 111 | * The transaction needs to be signed beforehand. 112 | * Returns the transaction broadcast result. 113 | * @param {string} tx the base64-encoded signed transaction 114 | * @param {BroadcastType} endpoint the transaction broadcast type (sync / commit) 115 | */ 116 | sendTransaction( 117 | tx: string, 118 | endpoint: K 119 | ): Promise['result']>; 120 | 121 | /** 122 | * Waits for the transaction to be committed on the chain. 123 | * NOTE: This method will not take in the fromHeight parameter once 124 | * proper transaction indexing is added - the implementation should 125 | * simply try to fetch the transaction first to see if it's included in a block 126 | * before starting to wait for it; Until then, this method should be used 127 | * in the sequence: 128 | * get latest block -> send transaction -> waitForTransaction(block before send) 129 | * @param {string} hash The transaction hash 130 | * @param {number} [fromHeight=latest] The block height used to begin the search 131 | * @param {number} [timeout=15000] Optional wait timeout in MS 132 | */ 133 | waitForTransaction( 134 | hash: string, 135 | fromHeight?: number, 136 | timeout?: number 137 | ): Promise; 138 | } 139 | -------------------------------------------------------------------------------- /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 account from the ABCI response 101 | * @param {string | null} abciData the base64-encoded ABCI data 102 | */ 103 | export const extractAccountFromResponse = ( 104 | abciData: string | null 105 | ): ABCIAccount => { 106 | // Make sure the response is initialized 107 | if (!abciData) { 108 | throw new Error('account is not initialized'); 109 | } 110 | 111 | try { 112 | // Parse the account 113 | const account: ABCIAccount = parseABCI(abciData); 114 | 115 | return account; 116 | } catch (e) { 117 | throw new Error('account is not initialized'); 118 | } 119 | }; 120 | 121 | /** 122 | * Extracts the simulate transaction response from the ABCI response value 123 | * @param {string | null} abciData the base64-encoded ResponseDeliverTx proto message 124 | */ 125 | export const extractSimulateFromResponse = ( 126 | abciResponse: ABCIResponse | null 127 | ): ResponseDeliverTx => { 128 | // Make sure the response is initialized 129 | if (!abciResponse) { 130 | throw new Error('abci data is not initialized'); 131 | } 132 | 133 | const error = abciResponse.response?.ResponseBase?.Error; 134 | if (error && error[ABCIErrorKey]) { 135 | throw constructRequestError(error[ABCIErrorKey]); 136 | } 137 | 138 | const value = abciResponse.response.Value; 139 | if (!value) { 140 | throw new Error('abci data is not initialized'); 141 | } 142 | 143 | try { 144 | return parseProto(value, ResponseDeliverTx.decode); 145 | } catch (e) { 146 | throw new Error('unable to parse simulate response'); 147 | } 148 | }; 149 | 150 | /** 151 | * Waits for the transaction to be committed to a block in the chain 152 | * of the specified provider. This helper does a search for incoming blocks 153 | * and checks if a transaction 154 | * @param {Provider} provider the provider instance 155 | * @param {string} hash the base64-encoded hash of the transaction 156 | * @param {number} [fromHeight=latest] the starting height for the search. If omitted, it is the latest block in the chain 157 | * @param {number} [timeout=15000] the timeout in MS for the search 158 | */ 159 | export const waitForTransaction = async ( 160 | provider: Provider, 161 | hash: string, 162 | fromHeight?: number, 163 | timeout?: number 164 | ): Promise => { 165 | return new Promise(async (resolve, reject) => { 166 | // Fetch the starting point 167 | let currentHeight = fromHeight 168 | ? fromHeight 169 | : await provider.getBlockNumber(); 170 | 171 | const exitTimeout = timeout ? timeout : 15000; 172 | 173 | const fetchInterval = setInterval(async () => { 174 | // Fetch the latest block height 175 | const latestHeight = await provider.getBlockNumber(); 176 | 177 | if (latestHeight < currentHeight) { 178 | // No need to parse older blocks 179 | return; 180 | } 181 | 182 | for (let blockNum = currentHeight; blockNum <= latestHeight; blockNum++) { 183 | // Fetch the block from the chain 184 | const block: BlockInfo = await provider.getBlock(blockNum); 185 | 186 | // Check if there are any transactions at all in the block 187 | if (!block.block.data.txs || block.block.data.txs.length == 0) { 188 | continue; 189 | } 190 | 191 | // Find the transaction among the block transactions 192 | for (const tx of block.block.data.txs) { 193 | // Decode the base-64 transaction 194 | const txRaw = base64ToUint8Array(tx); 195 | 196 | // Calculate the transaction hash 197 | const txHash = sha256(txRaw); 198 | 199 | if (uint8ArrayToBase64(txHash) == hash) { 200 | // Clear the interval 201 | clearInterval(fetchInterval); 202 | 203 | // Decode the transaction from amino 204 | resolve(Tx.decode(txRaw)); 205 | } 206 | } 207 | } 208 | 209 | currentHeight = latestHeight + 1; 210 | }, 1000); 211 | 212 | setTimeout(() => { 213 | // Clear the fetch interval 214 | clearInterval(fetchInterval); 215 | 216 | reject('transaction fetch timeout'); 217 | }, exitTimeout); 218 | }); 219 | }; 220 | -------------------------------------------------------------------------------- /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 | ABCIAccount, 13 | ABCIErrorKey, 14 | ABCIResponse, 15 | BlockInfo, 16 | BlockResult, 17 | BroadcastTransactionMap, 18 | BroadcastTxCommitResult, 19 | BroadcastTxSyncResult, 20 | ConsensusParams, 21 | NetworkInfo, 22 | RPCRequest, 23 | Status, 24 | TxResult, 25 | } from '../types'; 26 | import { 27 | extractAccountFromResponse, 28 | extractAccountNumberFromResponse, 29 | extractBalanceFromResponse, 30 | extractSequenceFromResponse, 31 | extractSimulateFromResponse, 32 | newRequest, 33 | uint8ArrayToBase64, 34 | waitForTransaction, 35 | } from '../utility'; 36 | import { constructRequestError } from '../utility/errors.utility'; 37 | 38 | /** 39 | * Provider based on JSON-RPC HTTP requests 40 | */ 41 | export class JSONRPCProvider implements Provider { 42 | protected readonly baseURL: string; 43 | 44 | /** 45 | * Creates a new instance of the JSON-RPC Provider 46 | * @param {string} baseURL the JSON-RPC URL of the node 47 | */ 48 | constructor(baseURL: string) { 49 | this.baseURL = baseURL; 50 | } 51 | 52 | async estimateGas(tx: Tx): Promise { 53 | const encodedTx = uint8ArrayToBase64(Tx.encode(tx).finish()); 54 | const abciResponse: ABCIResponse = await RestService.post( 55 | this.baseURL, 56 | { 57 | request: newRequest(ABCIEndpoint.ABCI_QUERY, [ 58 | `.app/simulate`, 59 | `${encodedTx}`, 60 | '0', // Height; not supported > 0 for now 61 | false, 62 | ]), 63 | } 64 | ); 65 | 66 | const simulateResult = extractSimulateFromResponse(abciResponse); 67 | 68 | return simulateResult.gas_used.toInt(); 69 | } 70 | 71 | async getBalance( 72 | address: string, 73 | denomination?: string, 74 | height?: number 75 | ): Promise { 76 | const abciResponse: ABCIResponse = await RestService.post( 77 | this.baseURL, 78 | { 79 | request: newRequest(ABCIEndpoint.ABCI_QUERY, [ 80 | `bank/balances/${address}`, 81 | '', 82 | '0', // Height; not supported > 0 for now 83 | false, 84 | ]), 85 | } 86 | ); 87 | 88 | return extractBalanceFromResponse( 89 | abciResponse.response.ResponseBase.Data, 90 | denomination ? denomination : 'ugnot' 91 | ); 92 | } 93 | 94 | async getBlock(height: number): Promise { 95 | return await RestService.post(this.baseURL, { 96 | request: newRequest(BlockEndpoint.BLOCK, [height.toString()]), 97 | }); 98 | } 99 | 100 | async getBlockResult(height: number): Promise { 101 | return await RestService.post(this.baseURL, { 102 | request: newRequest(BlockEndpoint.BLOCK_RESULTS, [height.toString()]), 103 | }); 104 | } 105 | 106 | async getBlockNumber(): Promise { 107 | // Fetch the status for the latest info 108 | const status = await this.getStatus(); 109 | 110 | return parseInt(status.sync_info.latest_block_height); 111 | } 112 | 113 | async getConsensusParams(height: number): Promise { 114 | return await RestService.post(this.baseURL, { 115 | request: newRequest(ConsensusEndpoint.CONSENSUS_PARAMS, [ 116 | height.toString(), 117 | ]), 118 | }); 119 | } 120 | 121 | getGasPrice(): Promise { 122 | return Promise.reject('not supported'); 123 | } 124 | 125 | async getNetwork(): Promise { 126 | return await RestService.post(this.baseURL, { 127 | request: newRequest(ConsensusEndpoint.NET_INFO), 128 | }); 129 | } 130 | 131 | async getAccountSequence(address: string, height?: number): Promise { 132 | const abciResponse: ABCIResponse = await RestService.post( 133 | this.baseURL, 134 | { 135 | request: newRequest(ABCIEndpoint.ABCI_QUERY, [ 136 | `auth/accounts/${address}`, 137 | '', 138 | '0', // Height; not supported > 0 for now 139 | false, 140 | ]), 141 | } 142 | ); 143 | 144 | return extractSequenceFromResponse(abciResponse.response.ResponseBase.Data); 145 | } 146 | 147 | async getAccountNumber(address: string, height?: number): Promise { 148 | const abciResponse: ABCIResponse = await RestService.post( 149 | this.baseURL, 150 | { 151 | request: newRequest(ABCIEndpoint.ABCI_QUERY, [ 152 | `auth/accounts/${address}`, 153 | '', 154 | '0', // Height; not supported > 0 for now 155 | false, 156 | ]), 157 | } 158 | ); 159 | 160 | return extractAccountNumberFromResponse( 161 | abciResponse.response.ResponseBase.Data 162 | ); 163 | } 164 | 165 | async getAccount(address: string, height?: number): Promise { 166 | const abciResponse: ABCIResponse = await RestService.post( 167 | this.baseURL, 168 | { 169 | request: newRequest(ABCIEndpoint.ABCI_QUERY, [ 170 | `auth/accounts/${address}`, 171 | '', 172 | '0', // Height; not supported > 0 for now 173 | false, 174 | ]), 175 | } 176 | ); 177 | 178 | return extractAccountFromResponse(abciResponse.response.ResponseBase.Data); 179 | } 180 | 181 | async getStatus(): Promise { 182 | return await RestService.post(this.baseURL, { 183 | request: newRequest(CommonEndpoint.STATUS, [null]), 184 | }); 185 | } 186 | 187 | async getTransaction(hash: string): Promise { 188 | return await RestService.post(this.baseURL, { 189 | request: newRequest(TransactionEndpoint.TX, [hash]), 190 | }); 191 | } 192 | 193 | async sendTransaction( 194 | tx: string, 195 | endpoint: K 196 | ): Promise { 197 | const request: RPCRequest = newRequest(endpoint, [tx]); 198 | 199 | switch (endpoint) { 200 | case TransactionEndpoint.BROADCAST_TX_COMMIT: 201 | // The endpoint is a commit broadcast 202 | // (it waits for the transaction to be committed) to the chain before returning 203 | return this.broadcastTxCommit(request); 204 | case TransactionEndpoint.BROADCAST_TX_SYNC: 205 | default: 206 | return this.broadcastTxSync(request); 207 | } 208 | } 209 | 210 | private async broadcastTxSync( 211 | request: RPCRequest 212 | ): Promise { 213 | const response: BroadcastTxSyncResult = 214 | await RestService.post(this.baseURL, { 215 | request, 216 | }); 217 | 218 | // Check if there is an immediate tx-broadcast error 219 | // (originating from basic transaction checks like CheckTx) 220 | if (response.error) { 221 | const errType: string = response.error[ABCIErrorKey]; 222 | const log: string = response.Log; 223 | 224 | throw constructRequestError(errType, log); 225 | } 226 | 227 | return response; 228 | } 229 | 230 | private async broadcastTxCommit( 231 | request: RPCRequest 232 | ): Promise { 233 | const response: BroadcastTxCommitResult = 234 | await RestService.post(this.baseURL, { 235 | request, 236 | }); 237 | 238 | const { check_tx, deliver_tx } = response; 239 | 240 | // Check if there is an immediate tx-broadcast error (in CheckTx) 241 | if (check_tx.ResponseBase.Error) { 242 | const errType: string = check_tx.ResponseBase.Error[ABCIErrorKey]; 243 | const log: string = check_tx.ResponseBase.Log; 244 | 245 | throw constructRequestError(errType, log); 246 | } 247 | 248 | // Check if there is a parsing error with the transaction (in DeliverTx) 249 | if (deliver_tx.ResponseBase.Error) { 250 | const errType: string = deliver_tx.ResponseBase.Error[ABCIErrorKey]; 251 | const log: string = deliver_tx.ResponseBase.Log; 252 | 253 | throw constructRequestError(errType, log); 254 | } 255 | 256 | return response; 257 | } 258 | 259 | async waitForTransaction( 260 | hash: string, 261 | fromHeight?: number, 262 | timeout?: number 263 | ): Promise { 264 | return waitForTransaction(this, hash, fromHeight, timeout); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /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/proto/google/protobuf/any.ts: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-ts_proto. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-ts_proto v2.7.7 4 | // protoc v5.29.3 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 | type_url: 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 { type_url: '', 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.type_url !== '') { 146 | writer.uint32(10).string(message.type_url); 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 | const 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.type_url = reader.string(); 168 | continue; 169 | } 170 | case 2: { 171 | if (tag !== 18) { 172 | break; 173 | } 174 | 175 | message.value = reader.bytes(); 176 | continue; 177 | } 178 | } 179 | if ((tag & 7) === 4 || tag === 0) { 180 | break; 181 | } 182 | reader.skip(tag & 7); 183 | } 184 | return message; 185 | }, 186 | 187 | fromJSON(object: any): Any { 188 | return { 189 | type_url: isSet(object.type_url) 190 | ? globalThis.String(object.type_url) 191 | : '', 192 | value: isSet(object.value) 193 | ? bytesFromBase64(object.value) 194 | : new Uint8Array(0), 195 | }; 196 | }, 197 | 198 | toJSON(message: Any): unknown { 199 | const obj: any = {}; 200 | if (message.type_url !== undefined) { 201 | obj.type_url = message.type_url; 202 | } 203 | if (message.value !== undefined) { 204 | obj.value = base64FromBytes(message.value); 205 | } 206 | return obj; 207 | }, 208 | 209 | create, I>>(base?: I): Any { 210 | return Any.fromPartial(base ?? ({} as any)); 211 | }, 212 | fromPartial, I>>(object: I): Any { 213 | const message = createBaseAny(); 214 | message.type_url = object.type_url ?? ''; 215 | message.value = object.value ?? new Uint8Array(0); 216 | return message; 217 | }, 218 | }; 219 | 220 | function bytesFromBase64(b64: string): Uint8Array { 221 | if ((globalThis as any).Buffer) { 222 | return Uint8Array.from(globalThis.Buffer.from(b64, 'base64')); 223 | } else { 224 | const bin = globalThis.atob(b64); 225 | const arr = new Uint8Array(bin.length); 226 | for (let i = 0; i < bin.length; ++i) { 227 | arr[i] = bin.charCodeAt(i); 228 | } 229 | return arr; 230 | } 231 | } 232 | 233 | function base64FromBytes(arr: Uint8Array): string { 234 | if ((globalThis as any).Buffer) { 235 | return globalThis.Buffer.from(arr).toString('base64'); 236 | } else { 237 | const bin: string[] = []; 238 | arr.forEach((byte) => { 239 | bin.push(globalThis.String.fromCharCode(byte)); 240 | }); 241 | return globalThis.btoa(bin.join('')); 242 | } 243 | } 244 | 245 | type Builtin = 246 | | Date 247 | | Function 248 | | Uint8Array 249 | | string 250 | | number 251 | | boolean 252 | | undefined; 253 | 254 | export type DeepPartial = T extends Builtin 255 | ? T 256 | : T extends Long 257 | ? string | number | Long 258 | : T extends globalThis.Array 259 | ? globalThis.Array> 260 | : T extends ReadonlyArray 261 | ? ReadonlyArray> 262 | : T extends {} 263 | ? { [K in keyof T]?: DeepPartial } 264 | : Partial; 265 | 266 | type KeysOfUnion = T extends T ? keyof T : never; 267 | export type Exact = P extends Builtin 268 | ? P 269 | : P & { [K in keyof P]: Exact } & { 270 | [K in Exclude>]: never; 271 | }; 272 | 273 | function isSet(value: any): boolean { 274 | return value !== null && value !== undefined; 275 | } 276 | 277 | export interface MessageFns { 278 | encode(message: T, writer?: BinaryWriter): BinaryWriter; 279 | decode(input: BinaryReader | Uint8Array, length?: number): T; 280 | fromJSON(object: any): T; 281 | toJSON(message: T): unknown; 282 | create, I>>(base?: I): T; 283 | fromPartial, I>>(object: I): T; 284 | } 285 | -------------------------------------------------------------------------------- /src/wallet/wallet.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ABCIAccount, 3 | BroadcastTxSyncResult, 4 | JSONRPCProvider, 5 | Status, 6 | TransactionEndpoint, 7 | } from '../provider'; 8 | import { mock } from 'jest-mock-extended'; 9 | import { SignTransactionOptions, Wallet } from './wallet'; 10 | import { EnglishMnemonic, Secp256k1 } from '@cosmjs/crypto'; 11 | import { 12 | defaultAddressPrefix, 13 | generateEntropy, 14 | generateKeyPair, 15 | } from './utility'; 16 | import { entropyToMnemonic } from '@cosmjs/crypto/build/bip39'; 17 | import { KeySigner } from './key'; 18 | import { Signer } from './signer'; 19 | import { Tx, TxSignature } from '../proto'; 20 | import Long from 'long'; 21 | import { Secp256k1PubKeyType } from './types'; 22 | import { Any } from '../proto/google/protobuf/any'; 23 | 24 | describe('Wallet', () => { 25 | test('createRandom', async () => { 26 | const wallet: Wallet = await Wallet.createRandom(); 27 | 28 | expect(wallet).not.toBeNull(); 29 | 30 | const address: string = await wallet.getAddress(); 31 | 32 | expect(address).toHaveLength(40); 33 | }); 34 | 35 | test('connect', async () => { 36 | const mockProvider = mock(); 37 | const wallet: Wallet = await Wallet.createRandom(); 38 | 39 | // Connect the provider 40 | wallet.connect(mockProvider); 41 | 42 | expect(wallet.getProvider()).toBe(mockProvider); 43 | }); 44 | 45 | test('fromMnemonic', async () => { 46 | const mnemonic: EnglishMnemonic = new EnglishMnemonic( 47 | 'lens balcony basic cherry half purchase balance soccer solar scissors process eager orchard fatigue rural retire approve crouch repair prepare develop clarify milk suffer' 48 | ); 49 | const wallet: Wallet = await Wallet.fromMnemonic(mnemonic.toString()); 50 | 51 | expect(wallet).not.toBeNull(); 52 | 53 | // Fetch the address 54 | const address: string = await wallet.getAddress(); 55 | 56 | expect(address).toBe( 57 | `${defaultAddressPrefix}1vcjvkjdvckprkcpm7l44plrtg83asfu9geaz90` 58 | ); 59 | }); 60 | 61 | test('fromPrivateKey', async () => { 62 | const { publicKey, privateKey } = await generateKeyPair( 63 | entropyToMnemonic(generateEntropy()), 64 | 0 65 | ); 66 | const signer: Signer = new KeySigner( 67 | privateKey, 68 | Secp256k1.compressPubkey(publicKey) 69 | ); 70 | 71 | const wallet: Wallet = await Wallet.fromPrivateKey(privateKey); 72 | const walletSigner: Signer = wallet.getSigner(); 73 | 74 | expect(wallet).not.toBeNull(); 75 | expect(await wallet.getAddress()).toBe(await signer.getAddress()); 76 | expect(await walletSigner.getPublicKey()).toEqual( 77 | await signer.getPublicKey() 78 | ); 79 | }); 80 | 81 | test('getAccountSequence', async () => { 82 | const mockSequence = 5; 83 | const mockProvider = mock(); 84 | mockProvider.getAccountSequence.mockResolvedValue(mockSequence); 85 | 86 | const wallet: Wallet = await Wallet.createRandom(); 87 | wallet.connect(mockProvider); 88 | 89 | const address: string = await wallet.getAddress(); 90 | const sequence: number = await wallet.getAccountSequence(); 91 | 92 | expect(mockProvider.getAccountSequence).toHaveBeenCalledWith( 93 | address, 94 | undefined 95 | ); 96 | expect(sequence).toBe(mockSequence); 97 | }); 98 | 99 | test('getAccountNumber', async () => { 100 | const mockAccountNumber = 10; 101 | const mockProvider = mock(); 102 | mockProvider.getAccountNumber.mockResolvedValue(mockAccountNumber); 103 | 104 | const wallet: Wallet = await Wallet.createRandom(); 105 | wallet.connect(mockProvider); 106 | 107 | const address: string = await wallet.getAddress(); 108 | const accountNumber: number = await wallet.getAccountNumber(); 109 | 110 | expect(mockProvider.getAccountNumber).toHaveBeenCalledWith( 111 | address, 112 | undefined 113 | ); 114 | expect(accountNumber).toBe(mockAccountNumber); 115 | }); 116 | 117 | test('getBalance', async () => { 118 | const mockBalance = 100; 119 | const mockProvider = mock(); 120 | mockProvider.getBalance.mockResolvedValue(mockBalance); 121 | 122 | const wallet: Wallet = await Wallet.createRandom(); 123 | wallet.connect(mockProvider); 124 | 125 | const address: string = await wallet.getAddress(); 126 | const balance: number = await wallet.getBalance(); 127 | 128 | expect(mockProvider.getBalance).toHaveBeenCalledWith(address, 'ugnot'); 129 | expect(balance).toBe(mockBalance); 130 | }); 131 | 132 | test('getGasPrice', async () => { 133 | const mockGasPrice = 1000; 134 | const mockProvider = mock(); 135 | mockProvider.getGasPrice.mockResolvedValue(mockGasPrice); 136 | 137 | const wallet: Wallet = await Wallet.createRandom(); 138 | wallet.connect(mockProvider); 139 | 140 | const gasPrice: number = await wallet.getGasPrice(); 141 | 142 | expect(mockProvider.getGasPrice).toHaveBeenCalled(); 143 | expect(gasPrice).toBe(mockGasPrice); 144 | }); 145 | 146 | test('estimateGas', async () => { 147 | const mockTxEstimation = 1000; 148 | const mockTx = mock(); 149 | const mockProvider = mock(); 150 | mockProvider.estimateGas.mockResolvedValue(mockTxEstimation); 151 | 152 | const wallet: Wallet = await Wallet.createRandom(); 153 | wallet.connect(mockProvider); 154 | 155 | const estimation: number = await wallet.estimateGas(mockTx); 156 | 157 | expect(mockProvider.estimateGas).toHaveBeenCalledWith(mockTx); 158 | expect(estimation).toBe(mockTxEstimation); 159 | }); 160 | 161 | test('signTransaction', async () => { 162 | const mockTx = mock(); 163 | mockTx.signatures = []; 164 | mockTx.fee = { 165 | gas_fee: '10', 166 | gas_wanted: new Long(10), 167 | }; 168 | mockTx.messages = []; 169 | 170 | const mockStatus = mock(); 171 | mockStatus.node_info = { 172 | version_set: [], 173 | version: '', 174 | net_address: '', 175 | software: '', 176 | channels: '', 177 | monkier: '', 178 | other: { 179 | tx_index: '', 180 | rpc_address: '', 181 | }, 182 | network: 'testchain', 183 | }; 184 | 185 | const mockProvider = mock(); 186 | mockProvider.getStatus.mockResolvedValue(mockStatus); 187 | const mockAccount: ABCIAccount = { 188 | BaseAccount: { 189 | address: '', 190 | coins: '', 191 | public_key: null, 192 | account_number: '', 193 | sequence: '', 194 | }, 195 | }; 196 | mockProvider.getAccount.mockResolvedValue(mockAccount); 197 | 198 | const wallet: Wallet = await Wallet.createRandom(); 199 | wallet.connect(mockProvider); 200 | 201 | const emptyDecodeCallback = (_: Any[]): any[] => { 202 | return []; 203 | }; 204 | const signedTx: Tx = await wallet.signTransaction( 205 | mockTx, 206 | emptyDecodeCallback 207 | ); 208 | 209 | expect(mockProvider.getStatus).toHaveBeenCalled(); 210 | expect(mockProvider.getAccount).toHaveBeenCalled(); 211 | 212 | expect(signedTx.signatures).toHaveLength(1); 213 | 214 | const sig: TxSignature = signedTx.signatures[0]; 215 | expect(sig.pub_key?.type_url).toBe(Secp256k1PubKeyType); 216 | expect(sig.pub_key?.value).not.toBeNull(); 217 | expect(sig.signature).not.toBeNull(); 218 | }); 219 | 220 | test('signTransactionWithAllOpts', async () => { 221 | const mockTx = mock(); 222 | mockTx.signatures = []; 223 | mockTx.fee = { 224 | gas_fee: '10', 225 | gas_wanted: new Long(10), 226 | }; 227 | mockTx.messages = []; 228 | 229 | const opts: SignTransactionOptions = { 230 | accountNumber: '42', 231 | sequence: '42', 232 | }; 233 | 234 | const mockStatus = mock(); 235 | mockStatus.node_info = { 236 | version_set: [], 237 | version: '', 238 | net_address: '', 239 | software: '', 240 | channels: '', 241 | monkier: '', 242 | other: { 243 | tx_index: '', 244 | rpc_address: '', 245 | }, 246 | network: 'testchain', 247 | }; 248 | 249 | const mockProvider = mock(); 250 | mockProvider.getStatus.mockResolvedValue(mockStatus); 251 | const mockAccount: ABCIAccount = { 252 | BaseAccount: { 253 | address: '', 254 | coins: '', 255 | public_key: null, 256 | account_number: '', 257 | sequence: '', 258 | }, 259 | }; 260 | mockProvider.getAccount.mockResolvedValue(mockAccount); 261 | 262 | const wallet: Wallet = await Wallet.createRandom(); 263 | wallet.connect(mockProvider); 264 | 265 | const emptyDecodeCallback = (_: Any[]): any[] => { 266 | return []; 267 | }; 268 | const signedTx: Tx = await wallet.signTransaction( 269 | mockTx, 270 | emptyDecodeCallback, 271 | opts 272 | ); 273 | 274 | expect(mockProvider.getStatus).toHaveBeenCalled(); 275 | expect(mockProvider.getAccount).not.toHaveBeenCalled(); 276 | 277 | expect(signedTx.signatures).toHaveLength(1); 278 | 279 | const sig: TxSignature = signedTx.signatures[0]; 280 | expect(sig.pub_key?.type_url).toBe(Secp256k1PubKeyType); 281 | expect(sig.pub_key?.value).not.toBeNull(); 282 | expect(sig.signature).not.toBeNull(); 283 | }); 284 | 285 | test('sendTransaction', async () => { 286 | const mockTx = mock(); 287 | mockTx.signatures = []; 288 | mockTx.fee = { 289 | gas_fee: '10', 290 | gas_wanted: new Long(10), 291 | }; 292 | mockTx.messages = []; 293 | mockTx.memo = ''; 294 | 295 | const mockTxHash = 'tx hash'; 296 | 297 | const mockStatus = mock(); 298 | mockStatus.node_info = { 299 | version_set: [], 300 | version: '', 301 | net_address: '', 302 | software: '', 303 | channels: '', 304 | monkier: '', 305 | other: { 306 | tx_index: '', 307 | rpc_address: '', 308 | }, 309 | network: 'testchain', 310 | }; 311 | 312 | const mockTransaction: BroadcastTxSyncResult = { 313 | error: null, 314 | data: null, 315 | Log: '', 316 | hash: mockTxHash, 317 | }; 318 | 319 | const mockProvider = mock(); 320 | mockProvider.getStatus.mockResolvedValue(mockStatus); 321 | mockProvider.getAccountNumber.mockResolvedValue(10); 322 | mockProvider.getAccountSequence.mockResolvedValue(10); 323 | mockProvider.sendTransaction.mockResolvedValue(mockTransaction); 324 | 325 | const wallet: Wallet = await Wallet.createRandom(); 326 | wallet.connect(mockProvider); 327 | 328 | const tx: BroadcastTxSyncResult = await wallet.sendTransaction( 329 | mockTx, 330 | TransactionEndpoint.BROADCAST_TX_SYNC 331 | ); 332 | 333 | expect(tx.hash).toBe(mockTxHash); 334 | }); 335 | }); 336 | -------------------------------------------------------------------------------- /src/proto/tm2/abci.ts: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-ts_proto. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-ts_proto v2.7.7 4 | // protoc v5.29.3 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 | response_base?: ResponseBase | undefined; 16 | gas_wanted: Long; 17 | gas_used: 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 { 30 | response_base: undefined, 31 | gas_wanted: Long.ZERO, 32 | gas_used: Long.ZERO, 33 | }; 34 | } 35 | 36 | export const ResponseDeliverTx: MessageFns = { 37 | encode( 38 | message: ResponseDeliverTx, 39 | writer: BinaryWriter = new BinaryWriter() 40 | ): BinaryWriter { 41 | if (message.response_base !== undefined) { 42 | ResponseBase.encode( 43 | message.response_base, 44 | writer.uint32(10).fork() 45 | ).join(); 46 | } 47 | if (!message.gas_wanted.equals(Long.ZERO)) { 48 | writer.uint32(16).sint64(message.gas_wanted.toString()); 49 | } 50 | if (!message.gas_used.equals(Long.ZERO)) { 51 | writer.uint32(24).sint64(message.gas_used.toString()); 52 | } 53 | return writer; 54 | }, 55 | 56 | decode(input: BinaryReader | Uint8Array, length?: number): ResponseDeliverTx { 57 | const reader = 58 | input instanceof BinaryReader ? input : new BinaryReader(input); 59 | const end = length === undefined ? reader.len : reader.pos + length; 60 | const message = createBaseResponseDeliverTx(); 61 | while (reader.pos < end) { 62 | const tag = reader.uint32(); 63 | switch (tag >>> 3) { 64 | case 1: { 65 | if (tag !== 10) { 66 | break; 67 | } 68 | 69 | message.response_base = ResponseBase.decode(reader, reader.uint32()); 70 | continue; 71 | } 72 | case 2: { 73 | if (tag !== 16) { 74 | break; 75 | } 76 | 77 | message.gas_wanted = Long.fromString(reader.sint64().toString()); 78 | continue; 79 | } 80 | case 3: { 81 | if (tag !== 24) { 82 | break; 83 | } 84 | 85 | message.gas_used = Long.fromString(reader.sint64().toString()); 86 | continue; 87 | } 88 | } 89 | if ((tag & 7) === 4 || tag === 0) { 90 | break; 91 | } 92 | reader.skip(tag & 7); 93 | } 94 | return message; 95 | }, 96 | 97 | fromJSON(object: any): ResponseDeliverTx { 98 | return { 99 | response_base: isSet(object.ResponseBase) 100 | ? ResponseBase.fromJSON(object.ResponseBase) 101 | : undefined, 102 | gas_wanted: isSet(object.GasWanted) 103 | ? Long.fromValue(object.GasWanted) 104 | : Long.ZERO, 105 | gas_used: isSet(object.GasUsed) 106 | ? Long.fromValue(object.GasUsed) 107 | : Long.ZERO, 108 | }; 109 | }, 110 | 111 | toJSON(message: ResponseDeliverTx): unknown { 112 | const obj: any = {}; 113 | if (message.response_base !== undefined) { 114 | obj.ResponseBase = ResponseBase.toJSON(message.response_base); 115 | } 116 | if (message.gas_wanted !== undefined) { 117 | obj.GasWanted = (message.gas_wanted || Long.ZERO).toString(); 118 | } 119 | if (message.gas_used !== undefined) { 120 | obj.GasUsed = (message.gas_used || Long.ZERO).toString(); 121 | } 122 | return obj; 123 | }, 124 | 125 | create, I>>( 126 | base?: I 127 | ): ResponseDeliverTx { 128 | return ResponseDeliverTx.fromPartial(base ?? ({} as any)); 129 | }, 130 | fromPartial, I>>( 131 | object: I 132 | ): ResponseDeliverTx { 133 | const message = createBaseResponseDeliverTx(); 134 | message.response_base = 135 | object.response_base !== undefined && object.response_base !== null 136 | ? ResponseBase.fromPartial(object.response_base) 137 | : undefined; 138 | message.gas_wanted = 139 | object.gas_wanted !== undefined && object.gas_wanted !== null 140 | ? Long.fromValue(object.gas_wanted) 141 | : Long.ZERO; 142 | message.gas_used = 143 | object.gas_used !== undefined && object.gas_used !== null 144 | ? Long.fromValue(object.gas_used) 145 | : Long.ZERO; 146 | return message; 147 | }, 148 | }; 149 | 150 | function createBaseResponseBase(): ResponseBase { 151 | return { 152 | error: undefined, 153 | data: new Uint8Array(0), 154 | events: [], 155 | log: '', 156 | info: '', 157 | }; 158 | } 159 | 160 | export const ResponseBase: MessageFns = { 161 | encode( 162 | message: ResponseBase, 163 | writer: BinaryWriter = new BinaryWriter() 164 | ): BinaryWriter { 165 | if (message.error !== undefined) { 166 | Any.encode(message.error, writer.uint32(10).fork()).join(); 167 | } 168 | if (message.data.length !== 0) { 169 | writer.uint32(18).bytes(message.data); 170 | } 171 | for (const v of message.events) { 172 | Any.encode(v!, writer.uint32(26).fork()).join(); 173 | } 174 | if (message.log !== '') { 175 | writer.uint32(34).string(message.log); 176 | } 177 | if (message.info !== '') { 178 | writer.uint32(42).string(message.info); 179 | } 180 | return writer; 181 | }, 182 | 183 | decode(input: BinaryReader | Uint8Array, length?: number): ResponseBase { 184 | const reader = 185 | input instanceof BinaryReader ? input : new BinaryReader(input); 186 | const end = length === undefined ? reader.len : reader.pos + length; 187 | const message = createBaseResponseBase(); 188 | while (reader.pos < end) { 189 | const tag = reader.uint32(); 190 | switch (tag >>> 3) { 191 | case 1: { 192 | if (tag !== 10) { 193 | break; 194 | } 195 | 196 | message.error = Any.decode(reader, reader.uint32()); 197 | continue; 198 | } 199 | case 2: { 200 | if (tag !== 18) { 201 | break; 202 | } 203 | 204 | message.data = reader.bytes(); 205 | continue; 206 | } 207 | case 3: { 208 | if (tag !== 26) { 209 | break; 210 | } 211 | 212 | message.events.push(Any.decode(reader, reader.uint32())); 213 | continue; 214 | } 215 | case 4: { 216 | if (tag !== 34) { 217 | break; 218 | } 219 | 220 | message.log = reader.string(); 221 | continue; 222 | } 223 | case 5: { 224 | if (tag !== 42) { 225 | break; 226 | } 227 | 228 | message.info = reader.string(); 229 | continue; 230 | } 231 | } 232 | if ((tag & 7) === 4 || tag === 0) { 233 | break; 234 | } 235 | reader.skip(tag & 7); 236 | } 237 | return message; 238 | }, 239 | 240 | fromJSON(object: any): ResponseBase { 241 | return { 242 | error: isSet(object.Error) ? Any.fromJSON(object.Error) : undefined, 243 | data: isSet(object.Data) 244 | ? bytesFromBase64(object.Data) 245 | : new Uint8Array(0), 246 | events: globalThis.Array.isArray(object?.Events) 247 | ? object.Events.map((e: any) => Any.fromJSON(e)) 248 | : [], 249 | log: isSet(object.Log) ? globalThis.String(object.Log) : '', 250 | info: isSet(object.Info) ? globalThis.String(object.Info) : '', 251 | }; 252 | }, 253 | 254 | toJSON(message: ResponseBase): unknown { 255 | const obj: any = {}; 256 | if (message.error !== undefined) { 257 | obj.Error = Any.toJSON(message.error); 258 | } 259 | if (message.data !== undefined) { 260 | obj.Data = base64FromBytes(message.data); 261 | } 262 | if (message.events?.length) { 263 | obj.Events = message.events.map((e) => Any.toJSON(e)); 264 | } 265 | if (message.log !== undefined) { 266 | obj.Log = message.log; 267 | } 268 | if (message.info !== undefined) { 269 | obj.Info = message.info; 270 | } 271 | return obj; 272 | }, 273 | 274 | create, I>>( 275 | base?: I 276 | ): ResponseBase { 277 | return ResponseBase.fromPartial(base ?? ({} as any)); 278 | }, 279 | fromPartial, I>>( 280 | object: I 281 | ): ResponseBase { 282 | const message = createBaseResponseBase(); 283 | message.error = 284 | object.error !== undefined && object.error !== null 285 | ? Any.fromPartial(object.error) 286 | : undefined; 287 | message.data = object.data ?? new Uint8Array(0); 288 | message.events = object.events?.map((e) => Any.fromPartial(e)) || []; 289 | message.log = object.log ?? ''; 290 | message.info = object.info ?? ''; 291 | return message; 292 | }, 293 | }; 294 | 295 | function bytesFromBase64(b64: string): Uint8Array { 296 | if ((globalThis as any).Buffer) { 297 | return Uint8Array.from(globalThis.Buffer.from(b64, 'base64')); 298 | } else { 299 | const bin = globalThis.atob(b64); 300 | const arr = new Uint8Array(bin.length); 301 | for (let i = 0; i < bin.length; ++i) { 302 | arr[i] = bin.charCodeAt(i); 303 | } 304 | return arr; 305 | } 306 | } 307 | 308 | function base64FromBytes(arr: Uint8Array): string { 309 | if ((globalThis as any).Buffer) { 310 | return globalThis.Buffer.from(arr).toString('base64'); 311 | } else { 312 | const bin: string[] = []; 313 | arr.forEach((byte) => { 314 | bin.push(globalThis.String.fromCharCode(byte)); 315 | }); 316 | return globalThis.btoa(bin.join('')); 317 | } 318 | } 319 | 320 | type Builtin = 321 | | Date 322 | | Function 323 | | Uint8Array 324 | | string 325 | | number 326 | | boolean 327 | | undefined; 328 | 329 | export type DeepPartial = T extends Builtin 330 | ? T 331 | : T extends Long 332 | ? string | number | Long 333 | : T extends globalThis.Array 334 | ? globalThis.Array> 335 | : T extends ReadonlyArray 336 | ? ReadonlyArray> 337 | : T extends {} 338 | ? { [K in keyof T]?: DeepPartial } 339 | : Partial; 340 | 341 | type KeysOfUnion = T extends T ? keyof T : never; 342 | export type Exact = P extends Builtin 343 | ? P 344 | : P & { [K in keyof P]: Exact } & { 345 | [K in Exclude>]: never; 346 | }; 347 | 348 | function isSet(value: any): boolean { 349 | return value !== null && value !== undefined; 350 | } 351 | 352 | export interface MessageFns { 353 | encode(message: T, writer?: BinaryWriter): BinaryWriter; 354 | decode(input: BinaryReader | Uint8Array, length?: number): T; 355 | fromJSON(object: any): T; 356 | toJSON(message: T): unknown; 357 | create, I>>(base?: I): T; 358 | fromPartial, I>>(object: I): T; 359 | } 360 | -------------------------------------------------------------------------------- /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 | export interface SignTransactionOptions { 29 | accountNumber?: string; 30 | sequence?: string; 31 | } 32 | 33 | /** 34 | * Wallet is a single account abstraction 35 | * that can interact with the blockchain 36 | */ 37 | export class Wallet { 38 | protected provider: Provider; 39 | protected signer: Signer; 40 | 41 | /** 42 | * Connects the wallet to the specified {@link Provider} 43 | * @param {Provider} provider the active {@link Provider}, if any 44 | */ 45 | connect = (provider: Provider) => { 46 | this.provider = provider; 47 | }; 48 | 49 | // Wallet initialization // 50 | 51 | /** 52 | * Generates a private key-based wallet, using a random seed 53 | * @param {AccountWalletOption} options the account options 54 | */ 55 | static createRandom = async ( 56 | options?: AccountWalletOption 57 | ): Promise => { 58 | const { publicKey, privateKey } = await generateKeyPair( 59 | entropyToMnemonic(generateEntropy()), 60 | 0 61 | ); 62 | 63 | // Initialize the wallet 64 | const wallet: Wallet = new Wallet(); 65 | wallet.signer = new KeySigner( 66 | privateKey, 67 | Secp256k1.compressPubkey(publicKey), 68 | options?.addressPrefix 69 | ); 70 | 71 | return wallet; 72 | }; 73 | 74 | /** 75 | * Generates a custom signer-based wallet 76 | * @param {Signer} signer the custom signer implementing the Signer interface 77 | * @param {CreateWalletOptions} options the wallet generation options 78 | */ 79 | static fromSigner = async (signer: Signer): Promise => { 80 | // Initialize the wallet 81 | const wallet: Wallet = new Wallet(); 82 | wallet.signer = signer; 83 | 84 | return wallet; 85 | }; 86 | 87 | /** 88 | * Generates a bip39 mnemonic-based wallet 89 | * @param {string} mnemonic the bip39 mnemonic 90 | * @param {CreateWalletOptions} options the wallet generation options 91 | */ 92 | static fromMnemonic = async ( 93 | mnemonic: string, 94 | options?: CreateWalletOptions 95 | ): Promise => { 96 | const { publicKey, privateKey } = await generateKeyPair( 97 | mnemonic, 98 | options?.accountIndex 99 | ); 100 | 101 | // Initialize the wallet 102 | const wallet: Wallet = new Wallet(); 103 | wallet.signer = new KeySigner( 104 | privateKey, 105 | Secp256k1.compressPubkey(publicKey), 106 | options?.addressPrefix 107 | ); 108 | 109 | return wallet; 110 | }; 111 | 112 | /** 113 | * Generates a private key-based wallet 114 | * @param {string} privateKey the private key 115 | * @param {AccountWalletOption} options the account options 116 | */ 117 | static fromPrivateKey = async ( 118 | privateKey: Uint8Array, 119 | options?: AccountWalletOption 120 | ): Promise => { 121 | // Derive the public key 122 | const { pubkey: publicKey } = await Secp256k1.makeKeypair(privateKey); 123 | 124 | // Initialize the wallet 125 | const wallet: Wallet = new Wallet(); 126 | wallet.signer = new KeySigner( 127 | privateKey, 128 | Secp256k1.compressPubkey(publicKey), 129 | options?.addressPrefix 130 | ); 131 | 132 | return wallet; 133 | }; 134 | 135 | /** 136 | * Creates a Ledger-based wallet 137 | * @param {LedgerConnector} connector the Ledger device connector 138 | * @param {CreateWalletOptions} options the wallet generation options 139 | */ 140 | static fromLedger = ( 141 | connector: LedgerConnector, 142 | options?: CreateWalletOptions 143 | ): Wallet => { 144 | const wallet: Wallet = new Wallet(); 145 | 146 | wallet.signer = new LedgerSigner( 147 | connector, 148 | options?.accountIndex ?? 0, 149 | options?.addressPrefix 150 | ); 151 | 152 | return wallet; 153 | }; 154 | 155 | // Account manipulation // 156 | 157 | /** 158 | * Fetches the address associated with the wallet 159 | */ 160 | getAddress = (): Promise => { 161 | return this.signer.getAddress(); 162 | }; 163 | 164 | /** 165 | * Fetches the account sequence for the wallet 166 | * @param {number} [height=latest] the block height 167 | */ 168 | getAccountSequence = async (height?: number): Promise => { 169 | if (!this.provider) { 170 | throw new Error('provider not connected'); 171 | } 172 | 173 | // Get the address 174 | const address: string = await this.getAddress(); 175 | 176 | return this.provider.getAccountSequence(address, height); 177 | }; 178 | 179 | /** 180 | * Fetches the account number for the wallet. Errors out if the 181 | * account is not initialized 182 | * @param {number} [height=latest] the block height 183 | */ 184 | getAccountNumber = async (height?: number): Promise => { 185 | if (!this.provider) { 186 | throw new Error('provider not connected'); 187 | } 188 | 189 | // Get the address 190 | const address: string = await this.getAddress(); 191 | 192 | return this.provider.getAccountNumber(address, height); 193 | }; 194 | 195 | /** 196 | * Fetches the account balance for the specific denomination 197 | * @param {string} [denomination=ugnot] the fund denomination 198 | */ 199 | getBalance = async (denomination?: string): Promise => { 200 | if (!this.provider) { 201 | throw new Error('provider not connected'); 202 | } 203 | 204 | // Get the address 205 | const address: string = await this.getAddress(); 206 | 207 | return this.provider.getBalance( 208 | address, 209 | denomination ? denomination : 'ugnot' 210 | ); 211 | }; 212 | 213 | /** 214 | * Fetches the current (recommended) average gas price 215 | */ 216 | getGasPrice = async (): Promise => { 217 | if (!this.provider) { 218 | throw new Error('provider not connected'); 219 | } 220 | 221 | return this.provider.getGasPrice(); 222 | }; 223 | 224 | /** 225 | * Estimates the gas limit for the transaction 226 | * @param {Tx} tx the transaction that needs estimating 227 | */ 228 | estimateGas = async (tx: Tx): Promise => { 229 | if (!this.provider) { 230 | throw new Error('provider not connected'); 231 | } 232 | 233 | return this.provider.estimateGas(tx); 234 | }; 235 | 236 | /** 237 | * Returns the connected provider, if any 238 | */ 239 | getProvider = (): Provider => { 240 | return this.provider; 241 | }; 242 | 243 | /** 244 | * Generates a transaction signature, and appends it to the transaction 245 | * @param {Tx} tx the transaction to be signed 246 | * @param {(messages: Any[]) => any[]} decodeTxMessages tx message decode callback 247 | * that should expand the concrete message fields into an object. Required because 248 | * the transaction sign bytes are generated using sorted JSON, which requires 249 | * encoded message values to be decoded for sorting 250 | */ 251 | signTransaction = async ( 252 | tx: Tx, 253 | decodeTxMessages: (messages: Any[]) => any[], 254 | opts?: SignTransactionOptions 255 | ): Promise => { 256 | if (!this.provider) { 257 | throw new Error('provider not connected'); 258 | } 259 | 260 | // Make sure the tx fee is initialized 261 | if (!tx.fee) { 262 | throw new Error('invalid transaction fee provided'); 263 | } 264 | 265 | // Extract the relevant chain data 266 | const status: Status = await this.provider.getStatus(); 267 | const chainID: string = status.node_info.network; 268 | 269 | // Extract the relevant account data 270 | let accountNumber = opts?.accountNumber; 271 | let accountSequence = opts?.sequence; 272 | if (accountNumber === undefined || accountSequence === undefined) { 273 | const address: string = await this.getAddress(); 274 | const account = await this.provider.getAccount(address); 275 | if (accountNumber === undefined) { 276 | accountNumber = account.BaseAccount.account_number; 277 | } 278 | if (accountSequence === undefined) { 279 | accountSequence = account.BaseAccount.sequence; 280 | } 281 | } 282 | const publicKey: Uint8Array = await this.signer.getPublicKey(); 283 | 284 | // Create the signature payload 285 | const signPayload: TxSignPayload = { 286 | chain_id: chainID, 287 | account_number: accountNumber, 288 | sequence: accountSequence, 289 | fee: { 290 | gas_fee: tx.fee.gas_fee, 291 | gas_wanted: tx.fee.gas_wanted.toString(10), 292 | }, 293 | msgs: decodeTxMessages(tx.messages), // unrolled message objects 294 | memo: tx.memo, 295 | }; 296 | 297 | // The TM2 node does signature verification using 298 | // a sorted JSON object, so the payload needs to be sorted 299 | // before signing 300 | const signBytes: Uint8Array = stringToUTF8( 301 | encodeCharacterSet(sortedJsonStringify(signPayload)) 302 | ); 303 | 304 | // The public key needs to be encoded using protobuf for Amino 305 | const wrappedKey: PubKeySecp256k1 = { 306 | key: publicKey, 307 | }; 308 | 309 | // Generate the signature 310 | const txSignature: TxSignature = { 311 | pub_key: { 312 | type_url: Secp256k1PubKeyType, 313 | value: PubKeySecp256k1.encode(wrappedKey).finish(), 314 | }, 315 | signature: await this.getSigner().signData(signBytes), 316 | }; 317 | 318 | // Append the signature 319 | return { 320 | ...tx, 321 | signatures: [...tx.signatures, txSignature], 322 | }; 323 | }; 324 | 325 | /** 326 | * Encodes and sends the transaction. If the type of endpoint 327 | * is a broadcast commit, waits for the transaction to be committed to the chain. 328 | * The transaction needs to be signed beforehand. 329 | * Returns the transaction hash (base-64) 330 | * @param {Tx} tx the signed transaction 331 | * @param {BroadcastType} endpoint the transaction broadcast type (sync / commit) 332 | */ 333 | async sendTransaction( 334 | tx: Tx, 335 | endpoint: K 336 | ): Promise { 337 | if (!this.provider) { 338 | throw new Error('provider not connected'); 339 | } 340 | 341 | // Encode the transaction to base-64 342 | const encodedTx: string = uint8ArrayToBase64(Tx.encode(tx).finish()); 343 | 344 | // Send the encoded transaction 345 | return this.provider.sendTransaction(encodedTx, endpoint); 346 | } 347 | 348 | /** 349 | * Returns the associated signer 350 | */ 351 | getSigner = (): Signer => { 352 | return this.signer; 353 | }; 354 | } 355 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ABCIAccount, 12 | ABCIErrorKey, 13 | ABCIResponse, 14 | BlockInfo, 15 | BlockResult, 16 | BroadcastTransactionMap, 17 | BroadcastTxCommitResult, 18 | BroadcastTxSyncResult, 19 | ConsensusParams, 20 | NetworkInfo, 21 | RPCRequest, 22 | RPCResponse, 23 | Status, 24 | TxResult, 25 | } from '../types'; 26 | import { 27 | extractAccountFromResponse, 28 | extractAccountNumberFromResponse, 29 | extractBalanceFromResponse, 30 | extractSequenceFromResponse, 31 | extractSimulateFromResponse, 32 | newRequest, 33 | uint8ArrayToBase64, 34 | waitForTransaction, 35 | } from '../utility'; 36 | import { constructRequestError } from '../utility/errors.utility'; 37 | 38 | /** 39 | * Provider based on WS JSON-RPC HTTP requests 40 | */ 41 | export class WSProvider implements Provider { 42 | protected ws: WebSocket; // the persistent WS connection 43 | protected readonly requestMap: Map< 44 | number | string, 45 | { 46 | resolve: (response: RPCResponse) => void; 47 | reject: (reason: Error) => void; 48 | timeout: NodeJS.Timeout; 49 | } 50 | > = new Map(); // callback method map for the individual endpoints 51 | protected requestTimeout = 15000; // 15s 52 | 53 | /** 54 | * Creates a new instance of the {@link WSProvider} 55 | * @param {string} baseURL the WS URL of the node 56 | * @param {number} requestTimeout the timeout for the WS request (in MS) 57 | */ 58 | constructor(baseURL: string, requestTimeout?: number) { 59 | this.ws = new WebSocket(baseURL); 60 | 61 | this.ws.addEventListener('message', (event) => { 62 | const response = JSON.parse(event.data as string) as RPCResponse; 63 | const request = this.requestMap.get(response.id); 64 | if (request) { 65 | this.requestMap.delete(response.id); 66 | clearTimeout(request.timeout); 67 | 68 | request.resolve(response); 69 | } 70 | 71 | // Set the default timeout 72 | this.requestTimeout = requestTimeout ? requestTimeout : 15000; 73 | }); 74 | } 75 | 76 | /** 77 | * Closes the WS connection. Required when done working 78 | * with the WS provider 79 | */ 80 | closeConnection() { 81 | this.ws.close(); 82 | } 83 | 84 | /** 85 | * Sends a request to the WS connection, and resolves 86 | * upon receiving the response 87 | * @param {RPCRequest} request the RPC request 88 | */ 89 | async sendRequest(request: RPCRequest): Promise> { 90 | // Make sure the connection is open 91 | if (this.ws.readyState != WebSocket.OPEN) { 92 | await this.waitForOpenConnection(); 93 | } 94 | 95 | // The promise will resolve as soon as the response is received 96 | const promise = new Promise>((resolve, reject) => { 97 | const timeout = setTimeout(() => { 98 | this.requestMap.delete(request.id); 99 | 100 | reject(new Error('Request timed out')); 101 | }, this.requestTimeout); 102 | 103 | this.requestMap.set(request.id, { resolve, reject, timeout }); 104 | }); 105 | 106 | this.ws.send(JSON.stringify(request)); 107 | 108 | return promise; 109 | } 110 | 111 | /** 112 | * Parses the result from the response 113 | * @param {RPCResponse} response the response to be parsed 114 | */ 115 | parseResponse(response: RPCResponse): Result { 116 | if (!response) { 117 | throw new Error('invalid response'); 118 | } 119 | 120 | if (response.error) { 121 | throw new Error(response.error?.message); 122 | } 123 | 124 | if (!response.result) { 125 | throw new Error('invalid response returned'); 126 | } 127 | 128 | return response.result; 129 | } 130 | 131 | /** 132 | * Waits for the WS connection to be established 133 | */ 134 | waitForOpenConnection = (): Promise => { 135 | return new Promise((resolve, reject) => { 136 | const maxNumberOfAttempts = 20; 137 | const intervalTime = 500; // ms 138 | 139 | let currentAttempt = 0; 140 | const interval = setInterval(() => { 141 | if (this.ws.readyState === WebSocket.OPEN) { 142 | clearInterval(interval); 143 | resolve(null); 144 | } 145 | 146 | currentAttempt++; 147 | if (currentAttempt >= maxNumberOfAttempts) { 148 | clearInterval(interval); 149 | reject(new Error('Unable to establish WS connection')); 150 | } 151 | }, intervalTime); 152 | }); 153 | }; 154 | 155 | async estimateGas(tx: Tx): Promise { 156 | const encodedTx = uint8ArrayToBase64(Tx.encode(tx).finish()); 157 | const response = await this.sendRequest( 158 | newRequest(ABCIEndpoint.ABCI_QUERY, [ 159 | `.app/simulate`, 160 | `${encodedTx}`, 161 | '0', // Height; not supported > 0 for now 162 | false, 163 | ]) 164 | ); 165 | 166 | // Parse the response 167 | const abciResponse = this.parseResponse(response); 168 | 169 | const simulateResult = extractSimulateFromResponse(abciResponse); 170 | 171 | const resultErrorKey = simulateResult.response_base?.error?.type_url; 172 | if (resultErrorKey) { 173 | throw constructRequestError(resultErrorKey); 174 | } 175 | 176 | return simulateResult.gas_used.toInt(); 177 | } 178 | 179 | async getBalance( 180 | address: string, 181 | denomination?: string, 182 | height?: number 183 | ): Promise { 184 | const response = await this.sendRequest( 185 | newRequest(ABCIEndpoint.ABCI_QUERY, [ 186 | `bank/balances/${address}`, 187 | '', 188 | '0', // Height; not supported > 0 for now 189 | false, 190 | ]) 191 | ); 192 | 193 | // Parse the response 194 | const abciResponse = this.parseResponse(response); 195 | 196 | return extractBalanceFromResponse( 197 | abciResponse.response.ResponseBase.Data, 198 | denomination ? denomination : 'ugnot' 199 | ); 200 | } 201 | 202 | async getBlock(height: number): Promise { 203 | const response = await this.sendRequest( 204 | newRequest(BlockEndpoint.BLOCK, [height.toString()]) 205 | ); 206 | 207 | return this.parseResponse(response); 208 | } 209 | 210 | async getBlockResult(height: number): Promise { 211 | const response = await this.sendRequest( 212 | newRequest(BlockEndpoint.BLOCK_RESULTS, [height.toString()]) 213 | ); 214 | 215 | return this.parseResponse(response); 216 | } 217 | 218 | async getBlockNumber(): Promise { 219 | // Fetch the status for the latest info 220 | const status = await this.getStatus(); 221 | 222 | return parseInt(status.sync_info.latest_block_height); 223 | } 224 | 225 | async getConsensusParams(height: number): Promise { 226 | const response = await this.sendRequest( 227 | newRequest(ConsensusEndpoint.CONSENSUS_PARAMS, [height.toString()]) 228 | ); 229 | 230 | return this.parseResponse(response); 231 | } 232 | 233 | getGasPrice(): Promise { 234 | return Promise.reject('implement me'); 235 | } 236 | 237 | async getNetwork(): Promise { 238 | const response = await this.sendRequest( 239 | newRequest(ConsensusEndpoint.NET_INFO) 240 | ); 241 | 242 | return this.parseResponse(response); 243 | } 244 | 245 | async getAccountSequence(address: string, height?: number): Promise { 246 | const response = await this.sendRequest( 247 | newRequest(ABCIEndpoint.ABCI_QUERY, [ 248 | `auth/accounts/${address}`, 249 | '', 250 | '0', // Height; not supported > 0 for now 251 | false, 252 | ]) 253 | ); 254 | 255 | // Parse the response 256 | const abciResponse = this.parseResponse(response); 257 | 258 | return extractSequenceFromResponse(abciResponse.response.ResponseBase.Data); 259 | } 260 | 261 | async getAccountNumber(address: string, height?: number): Promise { 262 | const response = await this.sendRequest( 263 | newRequest(ABCIEndpoint.ABCI_QUERY, [ 264 | `auth/accounts/${address}`, 265 | '', 266 | '0', // Height; not supported > 0 for now 267 | false, 268 | ]) 269 | ); 270 | 271 | // Parse the response 272 | const abciResponse = this.parseResponse(response); 273 | 274 | return extractAccountNumberFromResponse( 275 | abciResponse.response.ResponseBase.Data 276 | ); 277 | } 278 | 279 | async getAccount(address: string, height?: number): Promise { 280 | const response = await this.sendRequest( 281 | newRequest(ABCIEndpoint.ABCI_QUERY, [ 282 | `auth/accounts/${address}`, 283 | '', 284 | '0', // Height; not supported > 0 for now 285 | false, 286 | ]) 287 | ); 288 | 289 | // Parse the response 290 | const abciResponse = this.parseResponse(response); 291 | 292 | return extractAccountFromResponse(abciResponse.response.ResponseBase.Data); 293 | } 294 | 295 | async getStatus(): Promise { 296 | const response = await this.sendRequest( 297 | newRequest(CommonEndpoint.STATUS, [null]) 298 | ); 299 | 300 | return this.parseResponse(response); 301 | } 302 | 303 | async getTransaction(hash: string): Promise { 304 | const response = await this.sendRequest( 305 | newRequest(TransactionEndpoint.TX, [hash]) 306 | ); 307 | 308 | return this.parseResponse(response); 309 | } 310 | 311 | async sendTransaction( 312 | tx: string, 313 | endpoint: K 314 | ): Promise { 315 | const request: RPCRequest = newRequest(endpoint, [tx]); 316 | 317 | switch (endpoint) { 318 | case TransactionEndpoint.BROADCAST_TX_COMMIT: 319 | // The endpoint is a commit broadcast 320 | // (it waits for the transaction to be committed) to the chain before returning 321 | return this.broadcastTxCommit(request); 322 | case TransactionEndpoint.BROADCAST_TX_SYNC: 323 | default: 324 | return this.broadcastTxSync(request); 325 | } 326 | } 327 | 328 | private async broadcastTxSync( 329 | request: RPCRequest 330 | ): Promise { 331 | const response: RPCResponse = 332 | await this.sendRequest(request); 333 | 334 | const broadcastResponse: BroadcastTxSyncResult = 335 | this.parseResponse(response); 336 | 337 | // Check if there is an immediate tx-broadcast error 338 | // (originating from basic transaction checks like CheckTx) 339 | if (broadcastResponse.error) { 340 | const errType: string = broadcastResponse.error[ABCIErrorKey]; 341 | const log: string = broadcastResponse.Log; 342 | 343 | throw constructRequestError(errType, log); 344 | } 345 | 346 | return broadcastResponse; 347 | } 348 | 349 | private async broadcastTxCommit( 350 | request: RPCRequest 351 | ): Promise { 352 | const response: RPCResponse = 353 | await this.sendRequest(request); 354 | 355 | const broadcastResponse: BroadcastTxCommitResult = 356 | this.parseResponse(response); 357 | 358 | const { check_tx, deliver_tx } = broadcastResponse; 359 | 360 | // Check if there is an immediate tx-broadcast error (in CheckTx) 361 | if (check_tx.ResponseBase.Error) { 362 | const errType: string = check_tx.ResponseBase.Error[ABCIErrorKey]; 363 | const log: string = check_tx.ResponseBase.Log; 364 | 365 | throw constructRequestError(errType, log); 366 | } 367 | 368 | // Check if there is a parsing error with the transaction (in DeliverTx) 369 | if (deliver_tx.ResponseBase.Error) { 370 | const errType: string = deliver_tx.ResponseBase.Error[ABCIErrorKey]; 371 | const log: string = deliver_tx.ResponseBase.Log; 372 | 373 | throw constructRequestError(errType, log); 374 | } 375 | 376 | return broadcastResponse; 377 | } 378 | 379 | waitForTransaction( 380 | hash: string, 381 | fromHeight?: number, 382 | timeout?: number 383 | ): Promise { 384 | return waitForTransaction(this, hash, fromHeight, timeout); 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /src/proto/tm2/multisig.ts: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-ts_proto. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-ts_proto v2.7.7 4 | // protoc v5.29.3 5 | // source: tm2/multisig.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 = 'tm'; 13 | 14 | /** messages */ 15 | export interface PubKeyMultisig { 16 | k: Long; 17 | pub_keys: Any[]; 18 | } 19 | 20 | export interface Multisignature { 21 | bit_array?: CompactBitArray | undefined; 22 | sigs: Uint8Array[]; 23 | } 24 | 25 | export interface CompactBitArray { 26 | /** The number of extra bits in elems. */ 27 | extra_bits_stored: number; 28 | elems: Uint8Array; 29 | } 30 | 31 | function createBasePubKeyMultisig(): PubKeyMultisig { 32 | return { k: Long.UZERO, pub_keys: [] }; 33 | } 34 | 35 | export const PubKeyMultisig: MessageFns = { 36 | encode( 37 | message: PubKeyMultisig, 38 | writer: BinaryWriter = new BinaryWriter() 39 | ): BinaryWriter { 40 | if (!message.k.equals(Long.UZERO)) { 41 | writer.uint32(8).uint64(message.k.toString()); 42 | } 43 | for (const v of message.pub_keys) { 44 | Any.encode(v!, writer.uint32(18).fork()).join(); 45 | } 46 | return writer; 47 | }, 48 | 49 | decode(input: BinaryReader | Uint8Array, length?: number): PubKeyMultisig { 50 | const reader = 51 | input instanceof BinaryReader ? input : new BinaryReader(input); 52 | const end = length === undefined ? reader.len : reader.pos + length; 53 | const message = createBasePubKeyMultisig(); 54 | while (reader.pos < end) { 55 | const tag = reader.uint32(); 56 | switch (tag >>> 3) { 57 | case 1: { 58 | if (tag !== 8) { 59 | break; 60 | } 61 | 62 | message.k = Long.fromString(reader.uint64().toString(), true); 63 | continue; 64 | } 65 | case 2: { 66 | if (tag !== 18) { 67 | break; 68 | } 69 | 70 | message.pub_keys.push(Any.decode(reader, reader.uint32())); 71 | continue; 72 | } 73 | } 74 | if ((tag & 7) === 4 || tag === 0) { 75 | break; 76 | } 77 | reader.skip(tag & 7); 78 | } 79 | return message; 80 | }, 81 | 82 | fromJSON(object: any): PubKeyMultisig { 83 | return { 84 | k: isSet(object.threshold) 85 | ? Long.fromValue(object.threshold) 86 | : Long.UZERO, 87 | pub_keys: globalThis.Array.isArray(object?.pubkeys) 88 | ? object.pubkeys.map((e: any) => Any.fromJSON(e)) 89 | : [], 90 | }; 91 | }, 92 | 93 | toJSON(message: PubKeyMultisig): unknown { 94 | const obj: any = {}; 95 | if (message.k !== undefined) { 96 | obj.threshold = (message.k || Long.UZERO).toString(); 97 | } 98 | if (message.pub_keys?.length) { 99 | obj.pubkeys = message.pub_keys.map((e) => Any.toJSON(e)); 100 | } 101 | return obj; 102 | }, 103 | 104 | create, I>>( 105 | base?: I 106 | ): PubKeyMultisig { 107 | return PubKeyMultisig.fromPartial(base ?? ({} as any)); 108 | }, 109 | fromPartial, I>>( 110 | object: I 111 | ): PubKeyMultisig { 112 | const message = createBasePubKeyMultisig(); 113 | message.k = 114 | object.k !== undefined && object.k !== null 115 | ? Long.fromValue(object.k) 116 | : Long.UZERO; 117 | message.pub_keys = object.pub_keys?.map((e) => Any.fromPartial(e)) || []; 118 | return message; 119 | }, 120 | }; 121 | 122 | function createBaseMultisignature(): Multisignature { 123 | return { bit_array: undefined, sigs: [] }; 124 | } 125 | 126 | export const Multisignature: MessageFns = { 127 | encode( 128 | message: Multisignature, 129 | writer: BinaryWriter = new BinaryWriter() 130 | ): BinaryWriter { 131 | if (message.bit_array !== undefined) { 132 | CompactBitArray.encode( 133 | message.bit_array, 134 | writer.uint32(10).fork() 135 | ).join(); 136 | } 137 | for (const v of message.sigs) { 138 | writer.uint32(18).bytes(v!); 139 | } 140 | return writer; 141 | }, 142 | 143 | decode(input: BinaryReader | Uint8Array, length?: number): Multisignature { 144 | const reader = 145 | input instanceof BinaryReader ? input : new BinaryReader(input); 146 | const end = length === undefined ? reader.len : reader.pos + length; 147 | const message = createBaseMultisignature(); 148 | while (reader.pos < end) { 149 | const tag = reader.uint32(); 150 | switch (tag >>> 3) { 151 | case 1: { 152 | if (tag !== 10) { 153 | break; 154 | } 155 | 156 | message.bit_array = CompactBitArray.decode(reader, reader.uint32()); 157 | continue; 158 | } 159 | case 2: { 160 | if (tag !== 18) { 161 | break; 162 | } 163 | 164 | message.sigs.push(reader.bytes()); 165 | continue; 166 | } 167 | } 168 | if ((tag & 7) === 4 || tag === 0) { 169 | break; 170 | } 171 | reader.skip(tag & 7); 172 | } 173 | return message; 174 | }, 175 | 176 | fromJSON(object: any): Multisignature { 177 | return { 178 | bit_array: isSet(object.bit_array) 179 | ? CompactBitArray.fromJSON(object.bit_array) 180 | : undefined, 181 | sigs: globalThis.Array.isArray(object?.sigs) 182 | ? object.sigs.map((e: any) => bytesFromBase64(e)) 183 | : [], 184 | }; 185 | }, 186 | 187 | toJSON(message: Multisignature): unknown { 188 | const obj: any = {}; 189 | if (message.bit_array !== undefined) { 190 | obj.bit_array = CompactBitArray.toJSON(message.bit_array); 191 | } 192 | if (message.sigs?.length) { 193 | obj.sigs = message.sigs.map((e) => base64FromBytes(e)); 194 | } 195 | return obj; 196 | }, 197 | 198 | create, I>>( 199 | base?: I 200 | ): Multisignature { 201 | return Multisignature.fromPartial(base ?? ({} as any)); 202 | }, 203 | fromPartial, I>>( 204 | object: I 205 | ): Multisignature { 206 | const message = createBaseMultisignature(); 207 | message.bit_array = 208 | object.bit_array !== undefined && object.bit_array !== null 209 | ? CompactBitArray.fromPartial(object.bit_array) 210 | : undefined; 211 | message.sigs = object.sigs?.map((e) => e) || []; 212 | return message; 213 | }, 214 | }; 215 | 216 | function createBaseCompactBitArray(): CompactBitArray { 217 | return { extra_bits_stored: 0, elems: new Uint8Array(0) }; 218 | } 219 | 220 | export const CompactBitArray: MessageFns = { 221 | encode( 222 | message: CompactBitArray, 223 | writer: BinaryWriter = new BinaryWriter() 224 | ): BinaryWriter { 225 | if (message.extra_bits_stored !== 0) { 226 | writer.uint32(8).uint32(message.extra_bits_stored); 227 | } 228 | if (message.elems.length !== 0) { 229 | writer.uint32(18).bytes(message.elems); 230 | } 231 | return writer; 232 | }, 233 | 234 | decode(input: BinaryReader | Uint8Array, length?: number): CompactBitArray { 235 | const reader = 236 | input instanceof BinaryReader ? input : new BinaryReader(input); 237 | const end = length === undefined ? reader.len : reader.pos + length; 238 | const message = createBaseCompactBitArray(); 239 | while (reader.pos < end) { 240 | const tag = reader.uint32(); 241 | switch (tag >>> 3) { 242 | case 1: { 243 | if (tag !== 8) { 244 | break; 245 | } 246 | 247 | message.extra_bits_stored = reader.uint32(); 248 | continue; 249 | } 250 | case 2: { 251 | if (tag !== 18) { 252 | break; 253 | } 254 | 255 | message.elems = reader.bytes(); 256 | continue; 257 | } 258 | } 259 | if ((tag & 7) === 4 || tag === 0) { 260 | break; 261 | } 262 | reader.skip(tag & 7); 263 | } 264 | return message; 265 | }, 266 | 267 | fromJSON(json: any): CompactBitArray { 268 | if (json === null) { 269 | // Handle null case 270 | return createBaseCompactBitArray(); 271 | } 272 | 273 | if (typeof json !== 'string') { 274 | throw new Error( 275 | `CompactBitArray in JSON should be a string or null but got ${typeof json}` 276 | ); 277 | } 278 | 279 | const bits = json; 280 | const numBits = bits.length; 281 | 282 | // Create a new CompactBitArray 283 | const numBytes = Math.ceil(numBits / 8); 284 | const elems = new Uint8Array(numBytes); 285 | const extraBitsStored = numBits % 8; 286 | const bitArray = { extra_bits_stored: extraBitsStored, elems }; 287 | 288 | // Set bits based on the string representation 289 | for (let i = 0; i < numBits; i++) { 290 | if (bits[i] === 'x') { 291 | compactBitArraySetIndex(bitArray, i, true); 292 | } 293 | // For '_', we don't need to do anything as bits are initialized to 0 294 | } 295 | 296 | return bitArray; 297 | }, 298 | 299 | toJSON(message: CompactBitArray): unknown { 300 | throw new Error('not implemented'); 301 | }, 302 | 303 | create, I>>( 304 | base?: I 305 | ): CompactBitArray { 306 | return CompactBitArray.fromPartial(base ?? ({} as any)); 307 | }, 308 | fromPartial, I>>( 309 | object: I 310 | ): CompactBitArray { 311 | const message = createBaseCompactBitArray(); 312 | message.extra_bits_stored = object.extra_bits_stored ?? 0; 313 | message.elems = object.elems ?? new Uint8Array(0); 314 | return message; 315 | }, 316 | }; 317 | 318 | export function createCompactBitArray(bits: number): CompactBitArray { 319 | if (bits <= 0) { 320 | throw new Error('empty'); 321 | } 322 | 323 | const extraBitsStored = bits % 8; 324 | const elems = new Uint8Array(Math.ceil(bits / 8)); 325 | 326 | return { extra_bits_stored: extraBitsStored, elems }; 327 | } 328 | 329 | export function compactBitArraySize(bA: CompactBitArray): number { 330 | if (bA.elems === null) { 331 | return 0; 332 | } else if (bA.extra_bits_stored === 0) { 333 | return bA.elems.length * 8; 334 | } 335 | return (bA.elems.length - 1) * 8 + bA.extra_bits_stored; 336 | } 337 | 338 | // SetIndex sets the bit at index i within the bit array 339 | // Returns true if successful, false if out of bounds or array is null 340 | export function compactBitArraySetIndex( 341 | bA: CompactBitArray, 342 | i: number, 343 | v: boolean 344 | ): boolean { 345 | if (bA.elems === null) { 346 | return false; 347 | } 348 | 349 | if (i >= compactBitArraySize(bA)) { 350 | return false; 351 | } 352 | 353 | if (v) { 354 | // Set the bit (most significant bit first) 355 | bA.elems[i >> 3] |= 1 << (7 - (i % 8)); 356 | } else { 357 | // Clear the bit 358 | bA.elems[i >> 3] &= ~(1 << (7 - (i % 8))); 359 | } 360 | 361 | return true; 362 | } 363 | 364 | function bytesFromBase64(b64: string): Uint8Array { 365 | if ((globalThis as any).Buffer) { 366 | return Uint8Array.from(globalThis.Buffer.from(b64, 'base64')); 367 | } else { 368 | const bin = globalThis.atob(b64); 369 | const arr = new Uint8Array(bin.length); 370 | for (let i = 0; i < bin.length; ++i) { 371 | arr[i] = bin.charCodeAt(i); 372 | } 373 | return arr; 374 | } 375 | } 376 | 377 | function base64FromBytes(arr: Uint8Array): string { 378 | if ((globalThis as any).Buffer) { 379 | return globalThis.Buffer.from(arr).toString('base64'); 380 | } else { 381 | const bin: string[] = []; 382 | arr.forEach((byte) => { 383 | bin.push(globalThis.String.fromCharCode(byte)); 384 | }); 385 | return globalThis.btoa(bin.join('')); 386 | } 387 | } 388 | 389 | type Builtin = 390 | | Date 391 | | Function 392 | | Uint8Array 393 | | string 394 | | number 395 | | boolean 396 | | undefined; 397 | 398 | export type DeepPartial = T extends Builtin 399 | ? T 400 | : T extends Long 401 | ? string | number | Long 402 | : T extends globalThis.Array 403 | ? globalThis.Array> 404 | : T extends ReadonlyArray 405 | ? ReadonlyArray> 406 | : T extends {} 407 | ? { [K in keyof T]?: DeepPartial } 408 | : Partial; 409 | 410 | type KeysOfUnion = T extends T ? keyof T : never; 411 | export type Exact = P extends Builtin 412 | ? P 413 | : P & { [K in keyof P]: Exact } & { 414 | [K in Exclude>]: never; 415 | }; 416 | 417 | function isSet(value: any): boolean { 418 | return value !== null && value !== undefined; 419 | } 420 | 421 | export interface MessageFns { 422 | encode(message: T, writer?: BinaryWriter): BinaryWriter; 423 | decode(input: BinaryReader | Uint8Array, length?: number): T; 424 | fromJSON(object: any): T; 425 | toJSON(message: T): unknown; 426 | create, I>>(base?: I): T; 427 | fromPartial, I>>(object: I): T; 428 | } 429 | -------------------------------------------------------------------------------- /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 | describe('getAccount', () => { 379 | const validAccount: ABCIAccount = { 380 | BaseAccount: { 381 | address: 'random address', 382 | coins: '', 383 | public_key: { '@type': 'pktype', value: 'pk' }, 384 | account_number: '10', 385 | sequence: '42', 386 | }, 387 | }; 388 | 389 | test.each([ 390 | [JSON.stringify(validAccount), validAccount], // account exists 391 | ['null', null], // account doesn't exist 392 | ])('case %#', async (response, expected) => { 393 | const mockABCIResponse: ABCIResponse = mock(); 394 | mockABCIResponse.response.ResponseBase = { 395 | Log: '', 396 | Info: '', 397 | Data: stringToBase64(response), 398 | Error: null, 399 | Events: null, 400 | }; 401 | 402 | mockedAxios.post.mockResolvedValue({ 403 | data: newResponse(mockABCIResponse), 404 | }); 405 | 406 | try { 407 | // Create the provider 408 | const provider = new JSONRPCProvider(mockURL); 409 | const account = await provider.getAccount('address'); 410 | 411 | expect(axios.post).toHaveBeenCalled(); 412 | expect(account).toStrictEqual(expected); 413 | } catch (e) { 414 | expect((e as Error).message).toContain('account is not initialized'); 415 | } 416 | }); 417 | }); 418 | }); 419 | -------------------------------------------------------------------------------- /src/proto/tm2/tx.ts: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-ts_proto. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-ts_proto v2.7.7 4 | // protoc v5.29.3 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 | gas_wanted: Long; 28 | /** gas fee details () */ 29 | gas_fee: string; 30 | } 31 | 32 | export interface TxSignature { 33 | /** public key associated with the signature */ 34 | pub_key?: 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 | const 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 | } 80 | case 2: { 81 | if (tag !== 18) { 82 | break; 83 | } 84 | 85 | message.fee = TxFee.decode(reader, reader.uint32()); 86 | continue; 87 | } 88 | case 3: { 89 | if (tag !== 26) { 90 | break; 91 | } 92 | 93 | message.signatures.push(TxSignature.decode(reader, reader.uint32())); 94 | continue; 95 | } 96 | case 4: { 97 | if (tag !== 34) { 98 | break; 99 | } 100 | 101 | message.memo = reader.string(); 102 | continue; 103 | } 104 | } 105 | if ((tag & 7) === 4 || tag === 0) { 106 | break; 107 | } 108 | reader.skip(tag & 7); 109 | } 110 | return message; 111 | }, 112 | 113 | fromJSON(object: any): Tx { 114 | return { 115 | messages: globalThis.Array.isArray(object?.messages) 116 | ? object.messages.map((e: any) => Any.fromJSON(e)) 117 | : [], 118 | fee: isSet(object.fee) ? TxFee.fromJSON(object.fee) : undefined, 119 | signatures: globalThis.Array.isArray(object?.signatures) 120 | ? object.signatures.map((e: any) => TxSignature.fromJSON(e)) 121 | : [], 122 | memo: isSet(object.memo) ? globalThis.String(object.memo) : '', 123 | }; 124 | }, 125 | 126 | toJSON(message: Tx): unknown { 127 | const obj: any = {}; 128 | if (message.messages?.length) { 129 | obj.messages = message.messages.map((e) => Any.toJSON(e)); 130 | } 131 | if (message.fee !== undefined) { 132 | obj.fee = TxFee.toJSON(message.fee); 133 | } 134 | if (message.signatures?.length) { 135 | obj.signatures = message.signatures.map((e) => TxSignature.toJSON(e)); 136 | } 137 | if (message.memo !== undefined) { 138 | obj.memo = message.memo; 139 | } 140 | return obj; 141 | }, 142 | 143 | create, I>>(base?: I): Tx { 144 | return Tx.fromPartial(base ?? ({} as any)); 145 | }, 146 | fromPartial, I>>(object: I): Tx { 147 | const message = createBaseTx(); 148 | message.messages = object.messages?.map((e) => Any.fromPartial(e)) || []; 149 | message.fee = 150 | object.fee !== undefined && object.fee !== null 151 | ? TxFee.fromPartial(object.fee) 152 | : undefined; 153 | message.signatures = 154 | object.signatures?.map((e) => TxSignature.fromPartial(e)) || []; 155 | message.memo = object.memo ?? ''; 156 | return message; 157 | }, 158 | }; 159 | 160 | function createBaseTxFee(): TxFee { 161 | return { gas_wanted: Long.ZERO, gas_fee: '' }; 162 | } 163 | 164 | export const TxFee: MessageFns = { 165 | encode( 166 | message: TxFee, 167 | writer: BinaryWriter = new BinaryWriter() 168 | ): BinaryWriter { 169 | if (!message.gas_wanted.equals(Long.ZERO)) { 170 | writer.uint32(8).sint64(message.gas_wanted.toString()); 171 | } 172 | if (message.gas_fee !== '') { 173 | writer.uint32(18).string(message.gas_fee); 174 | } 175 | return writer; 176 | }, 177 | 178 | decode(input: BinaryReader | Uint8Array, length?: number): TxFee { 179 | const reader = 180 | input instanceof BinaryReader ? input : new BinaryReader(input); 181 | const end = length === undefined ? reader.len : reader.pos + length; 182 | const message = createBaseTxFee(); 183 | while (reader.pos < end) { 184 | const tag = reader.uint32(); 185 | switch (tag >>> 3) { 186 | case 1: { 187 | if (tag !== 8) { 188 | break; 189 | } 190 | 191 | message.gas_wanted = Long.fromString(reader.sint64().toString()); 192 | continue; 193 | } 194 | case 2: { 195 | if (tag !== 18) { 196 | break; 197 | } 198 | 199 | message.gas_fee = reader.string(); 200 | continue; 201 | } 202 | } 203 | if ((tag & 7) === 4 || tag === 0) { 204 | break; 205 | } 206 | reader.skip(tag & 7); 207 | } 208 | return message; 209 | }, 210 | 211 | fromJSON(object: any): TxFee { 212 | return { 213 | gas_wanted: isSet(object.gas_wanted) 214 | ? Long.fromValue(object.gas_wanted) 215 | : Long.ZERO, 216 | gas_fee: isSet(object.gas_fee) ? globalThis.String(object.gas_fee) : '', 217 | }; 218 | }, 219 | 220 | toJSON(message: TxFee): unknown { 221 | const obj: any = {}; 222 | if (message.gas_wanted !== undefined) { 223 | obj.gas_wanted = (message.gas_wanted || Long.ZERO).toString(); 224 | } 225 | if (message.gas_fee !== undefined) { 226 | obj.gas_fee = message.gas_fee; 227 | } 228 | return obj; 229 | }, 230 | 231 | create, I>>(base?: I): TxFee { 232 | return TxFee.fromPartial(base ?? ({} as any)); 233 | }, 234 | fromPartial, I>>(object: I): TxFee { 235 | const message = createBaseTxFee(); 236 | message.gas_wanted = 237 | object.gas_wanted !== undefined && object.gas_wanted !== null 238 | ? Long.fromValue(object.gas_wanted) 239 | : Long.ZERO; 240 | message.gas_fee = object.gas_fee ?? ''; 241 | return message; 242 | }, 243 | }; 244 | 245 | function createBaseTxSignature(): TxSignature { 246 | return { pub_key: undefined, signature: new Uint8Array(0) }; 247 | } 248 | 249 | export const TxSignature: MessageFns = { 250 | encode( 251 | message: TxSignature, 252 | writer: BinaryWriter = new BinaryWriter() 253 | ): BinaryWriter { 254 | if (message.pub_key !== undefined) { 255 | Any.encode(message.pub_key, writer.uint32(10).fork()).join(); 256 | } 257 | if (message.signature.length !== 0) { 258 | writer.uint32(18).bytes(message.signature); 259 | } 260 | return writer; 261 | }, 262 | 263 | decode(input: BinaryReader | Uint8Array, length?: number): TxSignature { 264 | const reader = 265 | input instanceof BinaryReader ? input : new BinaryReader(input); 266 | const end = length === undefined ? reader.len : reader.pos + length; 267 | const message = createBaseTxSignature(); 268 | while (reader.pos < end) { 269 | const tag = reader.uint32(); 270 | switch (tag >>> 3) { 271 | case 1: { 272 | if (tag !== 10) { 273 | break; 274 | } 275 | 276 | message.pub_key = Any.decode(reader, reader.uint32()); 277 | continue; 278 | } 279 | case 2: { 280 | if (tag !== 18) { 281 | break; 282 | } 283 | 284 | message.signature = reader.bytes(); 285 | continue; 286 | } 287 | } 288 | if ((tag & 7) === 4 || tag === 0) { 289 | break; 290 | } 291 | reader.skip(tag & 7); 292 | } 293 | return message; 294 | }, 295 | 296 | fromJSON(object: any): TxSignature { 297 | return { 298 | pub_key: isSet(object.pub_key) ? Any.fromJSON(object.pub_key) : undefined, 299 | signature: isSet(object.signature) 300 | ? bytesFromBase64(object.signature) 301 | : new Uint8Array(0), 302 | }; 303 | }, 304 | 305 | toJSON(message: TxSignature): unknown { 306 | const obj: any = {}; 307 | if (message.pub_key !== undefined) { 308 | obj.pub_key = Any.toJSON(message.pub_key); 309 | } 310 | if (message.signature !== undefined) { 311 | obj.signature = base64FromBytes(message.signature); 312 | } 313 | return obj; 314 | }, 315 | 316 | create, I>>(base?: I): TxSignature { 317 | return TxSignature.fromPartial(base ?? ({} as any)); 318 | }, 319 | fromPartial, I>>( 320 | object: I 321 | ): TxSignature { 322 | const message = createBaseTxSignature(); 323 | message.pub_key = 324 | object.pub_key !== undefined && object.pub_key !== null 325 | ? Any.fromPartial(object.pub_key) 326 | : undefined; 327 | message.signature = object.signature ?? new Uint8Array(0); 328 | return message; 329 | }, 330 | }; 331 | 332 | function createBasePubKeySecp256k1(): PubKeySecp256k1 { 333 | return { key: new Uint8Array(0) }; 334 | } 335 | 336 | export const PubKeySecp256k1: MessageFns = { 337 | encode( 338 | message: PubKeySecp256k1, 339 | writer: BinaryWriter = new BinaryWriter() 340 | ): BinaryWriter { 341 | if (message.key.length !== 0) { 342 | writer.uint32(10).bytes(message.key); 343 | } 344 | return writer; 345 | }, 346 | 347 | decode(input: BinaryReader | Uint8Array, length?: number): PubKeySecp256k1 { 348 | const reader = 349 | input instanceof BinaryReader ? input : new BinaryReader(input); 350 | const end = length === undefined ? reader.len : reader.pos + length; 351 | const message = createBasePubKeySecp256k1(); 352 | while (reader.pos < end) { 353 | const tag = reader.uint32(); 354 | switch (tag >>> 3) { 355 | case 1: { 356 | if (tag !== 10) { 357 | break; 358 | } 359 | 360 | message.key = reader.bytes(); 361 | continue; 362 | } 363 | } 364 | if ((tag & 7) === 4 || tag === 0) { 365 | break; 366 | } 367 | reader.skip(tag & 7); 368 | } 369 | return message; 370 | }, 371 | 372 | fromJSON(object: any): PubKeySecp256k1 { 373 | return { 374 | key: isSet(object.key) ? bytesFromBase64(object.key) : new Uint8Array(0), 375 | }; 376 | }, 377 | 378 | toJSON(message: PubKeySecp256k1): unknown { 379 | const obj: any = {}; 380 | if (message.key !== undefined) { 381 | obj.key = base64FromBytes(message.key); 382 | } 383 | return obj; 384 | }, 385 | 386 | create, I>>( 387 | base?: I 388 | ): PubKeySecp256k1 { 389 | return PubKeySecp256k1.fromPartial(base ?? ({} as any)); 390 | }, 391 | fromPartial, I>>( 392 | object: I 393 | ): PubKeySecp256k1 { 394 | const message = createBasePubKeySecp256k1(); 395 | message.key = object.key ?? new Uint8Array(0); 396 | return message; 397 | }, 398 | }; 399 | 400 | function bytesFromBase64(b64: string): Uint8Array { 401 | if ((globalThis as any).Buffer) { 402 | return Uint8Array.from(globalThis.Buffer.from(b64, 'base64')); 403 | } else { 404 | const bin = globalThis.atob(b64); 405 | const arr = new Uint8Array(bin.length); 406 | for (let i = 0; i < bin.length; ++i) { 407 | arr[i] = bin.charCodeAt(i); 408 | } 409 | return arr; 410 | } 411 | } 412 | 413 | function base64FromBytes(arr: Uint8Array): string { 414 | if ((globalThis as any).Buffer) { 415 | return globalThis.Buffer.from(arr).toString('base64'); 416 | } else { 417 | const bin: string[] = []; 418 | arr.forEach((byte) => { 419 | bin.push(globalThis.String.fromCharCode(byte)); 420 | }); 421 | return globalThis.btoa(bin.join('')); 422 | } 423 | } 424 | 425 | type Builtin = 426 | | Date 427 | | Function 428 | | Uint8Array 429 | | string 430 | | number 431 | | boolean 432 | | undefined; 433 | 434 | export type DeepPartial = T extends Builtin 435 | ? T 436 | : T extends Long 437 | ? string | number | Long 438 | : T extends globalThis.Array 439 | ? globalThis.Array> 440 | : T extends ReadonlyArray 441 | ? ReadonlyArray> 442 | : T extends {} 443 | ? { [K in keyof T]?: DeepPartial } 444 | : Partial; 445 | 446 | type KeysOfUnion = T extends T ? keyof T : never; 447 | export type Exact = P extends Builtin 448 | ? P 449 | : P & { [K in keyof P]: Exact } & { 450 | [K in Exclude>]: never; 451 | }; 452 | 453 | function isSet(value: any): boolean { 454 | return value !== null && value !== undefined; 455 | } 456 | 457 | export interface MessageFns { 458 | encode(message: T, writer?: BinaryWriter): BinaryWriter; 459 | decode(input: BinaryReader | Uint8Array, length?: number): T; 460 | fromJSON(object: any): T; 461 | toJSON(message: T): unknown; 462 | create, I>>(base?: I): T; 463 | fromPartial, I>>(object: I): T; 464 | } 465 | -------------------------------------------------------------------------------- /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('getAccount', () => { 264 | const validAccount: ABCIAccount = { 265 | BaseAccount: { 266 | address: 'random address', 267 | coins: '', 268 | public_key: null, 269 | account_number: '10', 270 | sequence: '0', 271 | }, 272 | }; 273 | 274 | test.each([ 275 | [JSON.stringify(validAccount), validAccount], // account exists 276 | ['null', null], // account doesn't exist 277 | ])('case %#', async (response, expected) => { 278 | const mockResponse: ABCIResponse = mockABCIResponse(response); 279 | 280 | // Set the response 281 | await setHandler(mockResponse); 282 | 283 | try { 284 | const account: ABCIAccount = await wsProvider.getAccount('address'); 285 | expect(account).toStrictEqual(expected); 286 | } catch (e) { 287 | expect((e as Error).message).toContain('account is not initialized'); 288 | } 289 | }); 290 | }); 291 | 292 | describe('getBalance', () => { 293 | const denomination = 'atom'; 294 | test.each([ 295 | ['"5gnot,100atom"', 100], // balance found 296 | ['"5universe"', 0], // balance not found 297 | ['""', 0], // account doesn't exist 298 | ])('case %#', async (existing, expected) => { 299 | const mockResponse: ABCIResponse = mockABCIResponse(existing); 300 | 301 | // Set the response 302 | await setHandler(mockResponse); 303 | 304 | const balance: number = await wsProvider.getBalance( 305 | 'address', 306 | denomination 307 | ); 308 | expect(balance).toBe(expected); 309 | }); 310 | }); 311 | 312 | test('getBlockNumber', async () => { 313 | const expectedBlockNumber = 10; 314 | const mockStatus: Status = getEmptyStatus(); 315 | mockStatus.sync_info.latest_block_height = `${expectedBlockNumber}`; 316 | 317 | // Set the response 318 | await setHandler(mockStatus); 319 | 320 | const blockNumber: number = await wsProvider.getBlockNumber(); 321 | expect(blockNumber).toBe(expectedBlockNumber); 322 | }); 323 | 324 | describe('sendTransaction', () => { 325 | const validResult: BroadcastTxSyncResult = { 326 | error: null, 327 | data: null, 328 | Log: '', 329 | hash: 'hash123', 330 | }; 331 | 332 | const mockError = '/std.UnauthorizedError'; 333 | const mockLog = 'random error message'; 334 | const invalidResult: BroadcastTxSyncResult = { 335 | error: { 336 | [ABCIErrorKey]: mockError, 337 | }, 338 | data: null, 339 | Log: mockLog, 340 | hash: '', 341 | }; 342 | 343 | test.each([ 344 | [validResult, validResult.hash, '', ''], // no error 345 | [invalidResult, invalidResult.hash, UnauthorizedErrorMessage, mockLog], // error out 346 | ])('case %#', async (response, expectedHash, expectedErr, expectedLog) => { 347 | await setHandler(response); 348 | 349 | try { 350 | const tx = await wsProvider.sendTransaction( 351 | 'encoded tx', 352 | TransactionEndpoint.BROADCAST_TX_SYNC 353 | ); 354 | 355 | expect(tx.hash).toEqual(expectedHash); 356 | 357 | if (expectedErr != '') { 358 | fail('expected error'); 359 | } 360 | } catch (e) { 361 | expect((e as Error).message).toBe(expectedErr); 362 | expect((e as TM2Error).log).toBe(expectedLog); 363 | } 364 | }); 365 | }); 366 | 367 | const getEmptyBlockInfo = (): BlockInfo => { 368 | const emptyHeader = { 369 | version: '', 370 | chain_id: '', 371 | height: '', 372 | time: '', 373 | num_txs: '', 374 | total_txs: '', 375 | app_version: '', 376 | last_block_id: { 377 | hash: null, 378 | parts: { 379 | total: '', 380 | hash: null, 381 | }, 382 | }, 383 | last_commit_hash: '', 384 | data_hash: '', 385 | validators_hash: '', 386 | consensus_hash: '', 387 | app_hash: '', 388 | last_results_hash: '', 389 | proposer_address: '', 390 | }; 391 | 392 | const emptyBlockID = { 393 | hash: null, 394 | parts: { 395 | total: '', 396 | hash: null, 397 | }, 398 | }; 399 | 400 | return { 401 | block_meta: { 402 | block_id: emptyBlockID, 403 | header: emptyHeader, 404 | }, 405 | block: { 406 | header: emptyHeader, 407 | data: { 408 | txs: null, 409 | }, 410 | last_commit: { 411 | block_id: emptyBlockID, 412 | precommits: null, 413 | }, 414 | }, 415 | }; 416 | }; 417 | 418 | test('getBlock', async () => { 419 | const mockInfo: BlockInfo = getEmptyBlockInfo(); 420 | 421 | // Set the response 422 | await setHandler(mockInfo); 423 | 424 | const result: BlockInfo = await wsProvider.getBlock(0); 425 | expect(result).toEqual(mockInfo); 426 | }); 427 | 428 | const getEmptyBlockResult = (): BlockResult => { 429 | const emptyResponseBase: ABCIResponseBase = { 430 | Error: null, 431 | Data: null, 432 | Events: null, 433 | Log: '', 434 | Info: '', 435 | }; 436 | 437 | const emptyEndBlock: EndBlock = { 438 | ResponseBase: emptyResponseBase, 439 | ValidatorUpdates: null, 440 | ConsensusParams: null, 441 | Events: null, 442 | }; 443 | 444 | const emptyStartBlock: BeginBlock = { 445 | ResponseBase: emptyResponseBase, 446 | }; 447 | 448 | return { 449 | height: '', 450 | results: { 451 | deliver_tx: null, 452 | end_block: emptyEndBlock, 453 | begin_block: emptyStartBlock, 454 | }, 455 | }; 456 | }; 457 | 458 | test('getBlockResult', async () => { 459 | const mockResult: BlockResult = getEmptyBlockResult(); 460 | 461 | // Set the response 462 | await setHandler(mockResult); 463 | 464 | const result: BlockResult = await wsProvider.getBlockResult(0); 465 | expect(result).toEqual(mockResult); 466 | }); 467 | 468 | test('waitForTransaction', async () => { 469 | const emptyBlock: BlockInfo = getEmptyBlockInfo(); 470 | emptyBlock.block.data = { 471 | txs: [], 472 | }; 473 | 474 | const tx: Tx = { 475 | messages: [], 476 | signatures: [], 477 | memo: 'tx memo', 478 | }; 479 | 480 | const encodedTx = Tx.encode(tx).finish(); 481 | const txHash = sha256(encodedTx); 482 | 483 | const filledBlock: BlockInfo = getEmptyBlockInfo(); 484 | filledBlock.block.data = { 485 | txs: [uint8ArrayToBase64(encodedTx)], 486 | }; 487 | 488 | const latestBlock = 5; 489 | const startBlock = latestBlock - 2; 490 | 491 | const mockStatus: Status = getEmptyStatus(); 492 | mockStatus.sync_info.latest_block_height = `${latestBlock}`; 493 | 494 | const responseMap: Map = new Map([ 495 | [latestBlock, filledBlock], 496 | [latestBlock - 1, emptyBlock], 497 | [startBlock, emptyBlock], 498 | ]); 499 | 500 | // Set the response 501 | server.on('connection', (socket) => { 502 | socket.on('message', (data) => { 503 | const request = JSON.parse(data.toString()); 504 | 505 | if (request.method == CommonEndpoint.STATUS) { 506 | const response = newResponse(mockStatus); 507 | response.id = request.id; 508 | 509 | socket.send(JSON.stringify(response)); 510 | 511 | return; 512 | } 513 | 514 | if (!request.params) { 515 | return; 516 | } 517 | 518 | const blockNum: number = +(request.params[0] as string[]); 519 | const info = responseMap.get(blockNum); 520 | 521 | const response = newResponse(info); 522 | response.id = request.id; 523 | 524 | socket.send(JSON.stringify(response)); 525 | }); 526 | }); 527 | 528 | await server.connected; 529 | 530 | const receivedTx: Tx = await wsProvider.waitForTransaction( 531 | uint8ArrayToBase64(txHash), 532 | startBlock 533 | ); 534 | expect(receivedTx).toEqual(tx); 535 | }); 536 | }); 537 | --------------------------------------------------------------------------------