diff --git a/README.md b/README.md index c14cb22982..42180cc6a5 100644 --- a/README.md +++ b/README.md @@ -109,179 +109,171 @@ The following blockchain libraries (generated by Telescope) are available via np ## Quickstart -Follow the instructions below to generate a new Typescript package that you can publish to npm. - -First, install `telescope` and `create-cosmos-app` +Follow the instructions below to generate a new Typescript package that you can publish to npm. You can also follow the video: https://youtu.be/iQf6p65fbdY +`create-interchain-app` ```sh -npm install -g @cosmology/telescope create-cosmos-app +npm install -g create-interchain-app ``` + ### Generate -Use the [`create-cosmos-app`](https://github.com/hyperweb-io/create-cosmos-app/) command to create a new package from the `telescope` boilerplate. +Use the [`create-interchain-app`](https://github.com/hyperweb-io/create-interchain-app/) command to create a new package from the `telescope` boilerplate. ```sh -cca --boilerplate telescope -``` +cia --boilerplate telescope -Then, you'll navigate into `./your-project/packages/telescope` package for the next steps. -You can also use `telescope generate` command to generate package according to the prompt or terminal command params such as: -`telescope generate --access public --userfullname testname --useremail test@gmail.com --module-desc test --username salkfl --license MIT --module-name test --chain-name cosmos --use-npm-scoped` - -The available options are: -`--userfullname` `--useremail` `--module-desc` `--username` `--module-name` `--chain-name` `--access` `--use-npm-scoped` `--license` +Then, you'll navigate into `./your-project/packages/telescope` package for the next steps. If some required options are missing, it will prompt to ask for the info. -To be noted, `--use-npm-scoped` only works when `--access` is `public` +For detailed cli `generate` commands, please check our docs and learn directories. -### Download protos with CLI +### Download protos The old ` telescope install ` command has been deprecated -You can use our CLI to download protos by using `download` command. +You can use our script to download protos by using `download-protos`command in the boilerplate. ```sh -telescope download +yarn download-protos ``` You should now see some repos cloned in `./git-modules` and proto files generated in `./protos`. These are the proto files downloaded according to your config. -Examples: +For detailed cli `download` commands, please check our docs and learn directories. -```sh -# Telescope will do the download according to .json file of --config -# Telescope will put proto into location specified by --out -telescope download --config ./protod.config.json --out ./git-modules -``` +### Transpile + +To create the Typescript files for your chain, run the `yarn codegen` command inside of the package. -```sh -# Telescope download from target repo according to --git-repo -# in format of (i.e. / or //) -# can be empty, it will use main as default -# Also --targets is required to specify the targets to download -# in format like cosmos/auth/v1beta1/auth.proto -telescope download --git-repo target-repo --targets target-proto ``` +yarn codegen +``` + +### Build + +Finally, run `install` and `build` to generate the JS and types for publishing your module to npm. ```sh -# ssh arg is optional, default is false -telescope download --config ./protod.config.json --out ./git-modules --ssh true +yarn build ``` +### Publishing -```js -// .protod.config.json example -// -// `repos` are the repository it's going to clone -// in format of (i.e. { "owner": , "repo": } or { "owner": , "repo": , "branch": }) -// can be empty, it will use main as default -// -// `protoDirMapping` is the directory of repo specified if the proto is not under repo's root directory ./protos -// in format of (i.e. / or //) -// can be empty, it will use main as default -// -// `outDir` is where the output proto will be put -// -// `targets` are the target proto to download -// `targets` can be patterns like: -// "cosmos/bank/v1beta1/tx.proto", -// "cosmos/gov/**/*.proto", -// "cosmos/authz/**/*.proto", -{ - "repos": [ - { "owner": "cosmos", "repo": "cosmos-sdk" }, - ... - ], - "protoDirMapping": { - "gogo/protobuf/master": ".", - ... - }, - "outDir": "protos", - "ssh": true, - "tempRepoDir": "git-modules", - "targets": [ - "cosmos/auth/v1beta1/auth.proto", - ... - ] -} -``` +Now you should have code inside of your `./src` folder, ready for publshing. If you used the `create-interchain-app` boilerplate, use `lerna` to publish (and/or read the README in the boilerplate for instructions), or run `npm publish` from your repository. -### Transpile +# Usage -To create the Typescript files for your chain, run the `yarn codegen` command inside of the package. +## Advanced Install +The methods below are all the options you can use to install and use Telescope +### Telescope CLI +Install telescope +```sh +npm install -g @cosmology/telescope ``` -yarn codegen -``` +The steps by order are: generate, download and transpile. -### Transpile with CLI +1.Generate a package with the telescope CLI: -Less recommended, but you can also use our CLI for transpilation. To create the Typescript files with the `cli`, run the `transpile` command. +Use and follow the default prompt: +```sh +telescope generate +``` +Or advanced cli option by your choice: ```sh -telescope transpile +telescope generate --access public --userfullname "Your Name" --useremail "your@email.com" --module-desc "Your module description" --username "your-username" --license MIT --module-name "your-module" --chain-name cosmos --use-npm-scoped ``` -You should now see some `.ts` files generated in `./src`. These are the real source files used in your application. +Available options: +- `--userfullname`: Your full name +- `--useremail`: Your email +- `--module-desc`: Module description +- `--username`: GitHub username +- `--module-name`: Module name +- `--chain-name`: Chain name +- `--access`: Package access (`public` or `private`) +- `--use-npm-scoped`: Use npm scoped package (only works with `--access public`) +- `--license`: License type + +2.Download protocol buffer files: +```sh +telescope download +``` -Examples: +This will clone repositories into `./git-modules` and generate proto files in `./protos`. +Download with a config file: ```sh -# Telescope takes chain1 folder as input, -# and generate files in 'gen/src' folder. -telescope transpile --protoDirs ../../__fixtures__/chain1 --outPath gen/src +telescope download --config ./protod.config.json --out ./git-modules ``` +Download from a specific repo: ```sh -# Telescope takes chain1 folder as input, -# and generate files in 'gen/src' folder using default telescope options. -telescope transpile --protoDirs ../../__fixtures__/chain1 --outPath gen/src --useDefaults +telescope download --git-repo owner/repository --targets cosmos/auth/v1beta1/auth.proto ``` +3. Transpile (Generate TypeScript code from proto files): +Use default telescope option: ```sh -# Telescope takes chain1 folder(from args) and chain2 folder(from config) as input, -# and generate files in 'gen/src'(defined in the config file, will override outPath in args) folder using a config file. -# Note: --config will override --useDefaults. -telescope transpile --protoDirs ../../__fixtures__/chain1 --config .telescope.json +telescope transpile ``` +Use customized telescope option: ```sh -# Telescope takes more than one config. The config afterward will override those in front. In this case values in .telescope-ext.json will override those in .telescope.json. -telescope transpile --config .telescope.json --config .telescope-ext.json +telescope transpile --config your-config.json ``` -```js -//.telescope.json -{ - "protoDirs": [ - "../../fixtures/chain2" - ], - "outPath": "gen/src", - "options": { - // telescope options - ... - } -} +### CIA +Please follow the video: https://youtu.be/iQf6p65fbdY +Or [Go to Quickstart](#quickstart) + +### CCA +First, install `create-cosmos-app` + +```sh +npm install -g create-cosmos-app ``` -### Build +Use the [`create-cosmos-app`](https://github.com/hyperweb-io/create-cosmos-app/) command to create a new package from the `telescope` boilerplate. -Finally, run `install` and `build` to generate the JS and types for publishing your module to npm. +```sh +cca --boilerplate telescope +``` +Then, you'll navigate into `./your-project/packages/telescope` package for the next steps. + +Install dependency and use cli to download the protos you want. ```sh -yarn build +yarn install +telescope download --config ./your.config.json ``` -### Publishing +To create the Typescript files for your chain, run the `yarn codegen` command inside of the package. -Now you should have code inside of your `./src` folder, ready for publshing. If you used the `create-cosmos-app` boilerplate, use `lerna` to publish (and/or read the README in the boilerplate for instructions), or run `npm publish` from your repository. +```sh +yarn codegen +``` -# Usage +### Manual install +If you want to use telescope in your own project. -## Programatic Usage +Run the command in ./your-project +```sh +yarn add --dev @cosmology/telescope +``` +Install helpers and cosmjs [dependencies listed here](#dependencies) + +We recommand to use [Go to Programatic Usage](#programatic-usage) + +You can also use [Go to Telescope Cli](#telescope-cli) +To be noted for cli command, add 'npx' or 'yarn' prefix when you use it within your project. For instance: 'yarn telescope generate', 'npx telescope download', etc. + +### Programatic Usage First add telescope to your `devDependencies`: @@ -291,6 +283,39 @@ yarn add --dev @cosmology/telescope Install helpers and cosmjs [dependencies listed here](#dependencies) +Download command example: +```js +import downloadProtos from '@cosmology/telescope/main/commands/download' + +const config = { + repos: [ + { owner: "cosmos", repo: "cosmos-sdk", branch: "release/v0.50.x" }, + { owner: "cosmos", repo: "ibc-go" }, + ], + protoDirMapping: { + "gogo/protobuf/master": ".", + "googleapis/googleapis/master": ".", + "protocolbuffers/protobuf/main": "src" + }, + outDir: "protos", + ssh: false, + tempRepoDir: "git-modules", + targets: [ + "cosmos/**/*.proto", + "ibc/**/*.proto", + ] +}; + +downloadProtos(config) + .then(() => console.log('✅ Proto download completed')) + // @ts-ignore + .catch((error) => { + console.error('❌ Proto download failed:', error); + process.exit(1); + }); +``` + +Transpile command example: ```js import { join } from 'path'; import telescope from '@cosmology/telescope'; @@ -1365,7 +1390,7 @@ A unified toolkit for building applications and smart contracts in the Interchai | **Wallet Connectors**| [**Interchain Kit**](https://github.com/hyperweb-io/interchain-kit)beta, [**Cosmos Kit**](https://github.com/hyperweb-io/cosmos-kit) | Experience the convenience of connecting with a variety of web3 wallets through a single, streamlined interface. | | **Signing Clients** | [**InterchainJS**](https://github.com/hyperweb-io/interchainjs)beta, [**CosmJS**](https://github.com/cosmos/cosmjs) | A single, universal signing interface for any network | | **SDK Clients** | [**Telescope**](https://github.com/hyperweb-io/telescope) | Your Frontend Companion for Building with TypeScript with Cosmos SDK Modules. | -| **Starter Kits** | [**Create Interchain App**](https://github.com/hyperweb-io/create-interchain-app)beta, [**Create Cosmos App**](https://github.com/hyperweb-io/create-cosmos-app) | Set up a modern Interchain app by running one command. | +| **Starter Kits** | [**Create Interchain App**](https://github.com/hyperweb-io/create-interchain-app)beta | Set up a modern Interchain app by running one command. | | **UI Kits** | [**Interchain UI**](https://github.com/hyperweb-io/interchain-ui) | The Interchain Design System, empowering developers with a flexible, easy-to-use UI kit. | | **Testing Frameworks** | [**Starship**](https://github.com/hyperweb-io/starship) | Unified Testing and Development for the Interchain. | | **TypeScript Smart Contracts** | [**Create Hyperweb App**](https://github.com/hyperweb-io/create-hyperweb-app) | Build and deploy full-stack blockchain applications with TypeScript | diff --git a/docs/_meta.json b/docs/_meta.json index c7a1518b86..36024190d6 100644 --- a/docs/_meta.json +++ b/docs/_meta.json @@ -1,15 +1,57 @@ { - "index": "Introduction", - "get-started": "Get Started", - "programatic-usage": "Programatic Usage", - "options": "Options", - "types": "Types", - "interfaces": "Interfaces", - "composing-messages": "Composing Messages", - "calculating-fees": "Calculating Fees", - "clients": "Clients", - "manually-registering-types": "Manually registering types", - "cosmwasm": "CosmWasm", - "dependencies": "Dependencies", - "developing": "Developing" -} + "label": "Docs", + "order": [ + "get-started", + "quickstart", + "clients", + "types", + "programatic-usage", + "options", + "manually-registering-types", + "composing-messages", + "calculating-fees", + "stargate-clients", + "rpc-clients", + "rpc-client-classes", + "lcd-clients", + "lcd-clients-classes", + "json-patch-protos", + "instant-rpc-methods", + "helper-functions-configuration", + "developing", + "creating-signers", + "dependencies", + "cosmwasm", + "broadcasting-messages", + "troubleshooting", + "sponsors", + "stack" + ], + "titles": { + "get-started": "Get Started", + "quickstart": "Quickstart", + "clients": "Clients", + "types": "Types", + "programatic-usage": "Programatic Usage", + "options": "Options", + "manually-registering-types": "Manually Registering Types", + "composing-messages": "Composing Messages", + "calculating-fees": "Calculating Fees", + "stargate-clients": "Stargate Clients", + "rpc-clients": "RPC Clients", + "rpc-client-classes": "RPC Client Classes", + "lcd-clients": "LCD Clients", + "lcd-clients-classes": "LCD Client Classes", + "json-patch-protos": "JSON Patch Protos", + "instant-rpc-methods": "Instant RPC Methods", + "helper-functions-configuration": "Helper Functions Configuration", + "developing": "Developing", + "creating-signers": "Creating Signers", + "dependencies": "Dependencies", + "cosmwasm": "CosmWasm", + "broadcasting-messages": "Broadcasting Messages", + "troubleshooting": "Troubleshooting", + "sponsors": "Sponsors", + "stack": "Stack" + } +} \ No newline at end of file diff --git a/docs/advanced-install-and-use.mdx b/docs/advanced-install-and-use.mdx new file mode 100644 index 0000000000..38a10938a0 --- /dev/null +++ b/docs/advanced-install-and-use.mdx @@ -0,0 +1,285 @@ +# Advanced Installation and Usage + +This document outlines all available methods to install and use Telescope. + +## Telescope CLI + +Install Telescope globally: + +```sh +npm install -g @cosmology/telescope +``` + +### Generate a Package + +Use the interactive prompt: + +```sh +telescope generate +``` + +Or specify options directly: + +```sh +telescope generate --access public --userfullname "Your Name" --useremail "your@email.com" --module-desc "Your module description" --username "your-username" --license MIT --module-name "your-module" --chain-name cosmos --use-npm-scoped +``` + +#### Available Options: +- `--userfullname`: Your full name +- `--useremail`: Your email +- `--module-desc`: Module description +- `--username`: GitHub username +- `--module-name`: Module name +- `--chain-name`: Chain name +- `--access`: Package access (`public` or `private`) +- `--use-npm-scoped`: Use npm scoped package (only works with `--access public`) +- `--license`: License type + +### Download Protocol Buffers + +Basic usage: + +```sh +telescope download +``` + +With a config file: + +```sh +telescope download --config ./protod.config.json --out ./git-modules +``` + +From a specific repository: + +```sh +telescope download --git-repo owner/repository --targets cosmos/auth/v1beta1/auth.proto +``` + +### Transpile Proto Files + +With default options: + +```sh +telescope transpile +``` + +With custom configuration: + +```sh +telescope transpile --config your-config.json +``` + +## Create Interchain App (CIA) + +Install CIA globally: + +```sh +npm install -g create-interchain-app +``` + +Create a new project with the Telescope boilerplate: + +```sh +cia --boilerplate telescope +``` + +Navigate to your project directory: + +```sh +cd ./your-project/packages/your-module +yarn install +``` + +Download protos and generate code: + +```sh +yarn download-protos +yarn codegen +``` + +## Create Cosmos App (CCA) + +Install CCA globally: + +```sh +npm install -g create-cosmos-app +``` + +Create a new package with the Telescope boilerplate: + +```sh +cca --boilerplate telescope +``` + +Navigate to your package directory: + +```sh +cd ./your-project/packages/telescope +yarn install +``` + +Download protos and generate code: + +```sh +telescope download --config ./your.config.json +yarn codegen +``` + +## Manual Installation + +Add Telescope to your project: + +```sh +yarn add --dev @cosmology/telescope +``` + +Install required dependencies: + +```sh +yarn add @cosmjs/amino @cosmjs/proto-signing @cosmjs/stargate @cosmjs/tendermint-rpc +``` + +Use either the programmatic API or the CLI with npm/yarn prefixes: + +```sh +yarn telescope generate +npx telescope download +``` + +## Programmatic Usage + +First, add Telescope and dependencies: + +```sh +yarn add --dev @cosmology/telescope +yarn add @cosmjs/amino @cosmjs/proto-signing @cosmjs/stargate @cosmjs/tendermint-rpc +``` + +### Downloading Protos Programmatically + +```js +import downloadProtos from '@cosmology/telescope/main/commands/download' + +const config = { + repos: [ + { owner: "cosmos", repo: "cosmos-sdk", branch: "release/v0.50.x" }, + { owner: "cosmos", repo: "ibc-go" }, + ], + protoDirMapping: { + "gogo/protobuf/master": ".", + "googleapis/googleapis/master": ".", + "protocolbuffers/protobuf/main": "src" + }, + outDir: "protos", + ssh: false, + tempRepoDir: "git-modules", + targets: [ + "cosmos/**/*.proto", + "ibc/**/*.proto", + ] +}; + +downloadProtos(config) + .then(() => console.log('✅ Proto download completed')) + // @ts-ignore + .catch((error) => { + console.error('❌ Proto download failed:', error); + process.exit(1); + }); +``` + +### Generating Code Programmatically + +```js +import { join } from 'path'; +import telescope from '@cosmology/telescope'; +import { sync as rimraf } from 'rimraf'; + +// Define input and output paths +const protoDirs = [join(__dirname, '/../proto')]; +const outPath = join(__dirname, '../src'); +rimraf(outPath); + +// Run Telescope with options +telescope({ + protoDirs, + outPath, + + // all options are totally optional + options: { + aminoEncoding: { + enabled: true + }, + lcdClients: { + enabled: false + }, + rpcClients: { + enabled: false, + camelCase: true + }, + + // Package-specific options + packages: { + nebula: { + prototypes: { + typingsFormat: { + useExact: false + } + } + }, + akash: { + stargateClients: { + enabled: true, + includeCosmosDefaultTypes: false + }, + prototypes: { + typingsFormat: { + useExact: false + } + } + } + } + } +}).then(() => { + console.log('✨ all done!'); +}).catch(e => { + console.error(e); + process.exit(1); +}); +``` + +### Example: Build Script Integration + +```js +// scripts/codegen.js +import { join } from 'path'; +import telescope from '@cosmology/telescope'; +import { sync as rimraf } from 'rimraf'; + +const protoDirs = [join(__dirname, '/../proto')]; +const outPath = join(__dirname, '../src/generated'); +rimraf(outPath); + +telescope({ + protoDirs, + outPath, + options: { + tsDisable: { + disableAll: false, + patterns: ['**/amino/**'] + }, + eslintDisable: { + patterns: ['**/tx.amino.ts'] + }, + prototypes: { + includePackageVar: true, + typingsFormat: { + useDeepPartial: true, + timestamp: 'date', + duration: 'duration' + } + } + } +}).then(() => { + console.log('✨ Code generation complete'); +}); \ No newline at end of file diff --git a/docs/broadcasting-messages.mdx b/docs/broadcasting-messages.mdx new file mode 100644 index 0000000000..296414d953 --- /dev/null +++ b/docs/broadcasting-messages.mdx @@ -0,0 +1,213 @@ +# Broadcasting Messages + +This document provides a reference for broadcasting transactions with messages in Telescope-generated clients. + +## Transaction Broadcasting Methods + +| Method | Description | Best For | +| ------ | ----------- | -------- | +| `signAndBroadcast` | Signs and broadcasts transactions in one step | Most use cases | +| `sign` + `broadcastTx` | Separates signing and broadcasting steps | Multi-signature or offline signing | +| `signAndBroadcastWithoutBalanceCheck` | Skips balance check for speedier execution | When balance is known to be sufficient | + +## Using SigningStargateClient + +The standard way to broadcast transactions: + +```typescript +import { SigningStargateClient } from "@cosmjs/stargate"; + +// Create a SigningStargateClient (see Creating Signers documentation) +const client = await SigningStargateClient.connectWithSigner(rpcEndpoint, signer, options); + +// Broadcast a transaction +const result = await client.signAndBroadcast( + signerAddress, + messages, + fee, + memo +); +``` + +## Telescope Convenience Methods + +```typescript +import { getSigningClient } from "./my-chain-client"; + +// Create a chain-specific client +const client = await getSigningClient({ + rpcEndpoint, + signer +}); + +// Broadcast a transaction using convenience method +const result = await client.sendMessages( + signerAddress, + messages, + fee, + memo +); +``` + +## Broadcasting Options + +| Option | Description | Default | +| ------ | ----------- | ------- | +| `broadcastTimeoutMs` | Maximum time to wait for confirmation | 60000 | +| `broadcastPollIntervalMs` | Time between broadcast status checks | 3000 | +| `gasAdjustment` | Multiplier for simulated gas | 1.5 | +| `gasPrice` | Price per unit of gas | Chain-specific | + +## Transaction Result Structure + +```typescript +interface BroadcastTxResult { + readonly height: number; // Block height at which tx was committed + readonly transactionHash: string; // The hash of the transaction + readonly rawLog: string; // Raw log information + readonly code?: number; // 0 for success, error code otherwise + readonly data?: Uint8Array; // Return value from transaction + readonly gasUsed: number; // Amount of gas used + readonly gasWanted: number; // Amount of gas requested + readonly events: readonly Event[]; // Events emitted by the transaction +} +``` + +## Broadcasting Workflow + +1. **Preparation**: Create messages and fee +2. **Simulation** (optional): Estimate gas +3. **Signing**: Create signature using signer +4. **Broadcasting**: Send to network +5. **Confirmation**: Wait for inclusion in a block +6. **Result Processing**: Handle success/failure + +## Transaction Simulation + +```typescript +// Simulate to estimate gas requirements +const gasEstimated = await client.simulate( + signerAddress, + messages, + memo +); + +// Apply safety factor +const gasLimit = Math.round(gasEstimated * 1.3); + +// Create fee with estimated gas +const fee = { + amount: [{ denom: "uatom", amount: "5000" }], + gas: gasLimit.toString() +}; +``` + +## Broadcast Modes + +| Mode | Description | Use Case | +| ---- | ----------- | -------- | +| `block` | Wait for block inclusion | Standard usage, ensures transaction success | +| `sync` | Return after mempool validation | Faster response, when block confirmation not needed | +| `async` | Return immediately | Maximum speed, no validation | + +```typescript +// Set broadcast mode (applicable for direct ABCI clients) +const result = await client.broadcastTx( + signedTx, + timeoutMs, + broadcastMode +); +``` + +## Error Handling + +```typescript +try { + const result = await client.signAndBroadcast( + signerAddress, + messages, + fee, + memo + ); + + if (result.code === 0) { + console.log("Transaction successful"); + } else { + console.error(`Transaction failed with code ${result.code}: ${result.rawLog}`); + } +} catch (error) { + if (error.message.includes("insufficient funds")) { + console.error("Account has insufficient funds to pay for fees"); + } else if (error.message.includes("out of gas")) { + console.error("Transaction ran out of gas"); + } else if (error.message.includes("account sequence mismatch")) { + console.error("Account sequence mismatch, retry with updated sequence"); + } else { + console.error("Broadcasting error:", error); + } +} +``` + +## Monitoring Transaction Status + +```typescript +// Get transaction status after broadcasting +const txStatus = await client.getTx(txHash); + +// Check if transaction was included in a block +if (txStatus) { + console.log("Transaction included at height:", txStatus.height); + console.log("Transaction success:", txStatus.code === 0); +} else { + console.log("Transaction not yet included in a block"); +} +``` + +## Sequence Number Management + +```typescript +// Get account data to check sequence +const account = await client.getAccount(signerAddress); +if (!account) { + throw new Error("Account not found"); +} + +console.log("Current sequence:", account.sequence); + +// For manual sequence management in advanced cases +const signDoc = { + chainId, + accountNumber: account.accountNumber, + sequence: account.sequence, + fee, + msgs: messages, + memo +}; +``` + +## Multi-Signature Transactions + +```typescript +// Collect signatures offline +const signatures = await Promise.all( + signers.map(signer => signer.sign(signerAddress, signDoc)) +); + +// Combine signatures +const combinedSignature = combineSignatures(signatures); + +// Broadcast with combined signature +const signedTx = createSignedTx(signDoc, combinedSignature); +const result = await client.broadcastTx(signedTx); +``` + +## Best Practices + +1. Always simulate transactions first to estimate gas +2. Apply a safety margin to estimated gas (1.3-1.5x) +3. Handle account sequence mismatches with retries +4. Verify transaction success by checking result code +5. Store transaction hashes for later queries +6. Use appropriate broadcast modes for your use case +7. Implement retry logic for transient failures +8. Monitor network congestion and adjust gas prices \ No newline at end of file diff --git a/docs/calculating-fees.mdx b/docs/calculating-fees.mdx index 717420bf0a..9e288638c5 100644 --- a/docs/calculating-fees.mdx +++ b/docs/calculating-fees.mdx @@ -1,27 +1,222 @@ -## Calculating Fees +# Calculating Fees -Make sure to create a `fee` object in addition to your message. +This document provides a reference for calculating transaction fees for Cosmos SDK blockchains using Telescope-generated types. -```js -import { coins } from '@cosmjs/amino'; +## Fee Structure -const fee = { - amount: coins(0, 'uosmo'), - gas: '250000' +| Component | Description | +| --------- | ----------- | +| Gas | Computational resources required to process a transaction | +| Gas Price | Price per unit of gas in a specific denomination | +| Fee | Total amount paid for transaction processing (Gas × Gas Price) | + +## Fee Object Structure + +```typescript +export interface StdFee { + readonly amount: readonly Coin[]; + readonly gas: string; } + +export interface Coin { + readonly denom: string; + readonly amount: string; +} +``` + +## Gas Estimation Methods + +| Method | Description | Usage | +| ------ | ----------- | ----- | +| Manual Specification | Explicitly set gas limit | Simple transactions with known gas requirements | +| Simulation | Estimate gas by simulating transaction | Complex transactions with variable gas needs | +| Auto | Automatically estimate and adjust gas | General-purpose usage | + +## Manual Fee Calculation + +```typescript +// Define a standard fee with fixed gas +const fee = { + amount: [{ denom: "uatom", amount: "5000" }], + gas: "200000" // 200,000 gas units +}; +``` + +## Using Gas Prices + +```typescript +import { calculateFee, GasPrice } from "@cosmjs/stargate"; + +// Define gas price (e.g., 0.025 uatom per gas unit) +const gasPrice = GasPrice.fromString("0.025uatom"); + +// Calculate fee for a given gas limit +const fee = calculateFee(200000, gasPrice); +// Result: { amount: [{ denom: "uatom", amount: "5000" }], gas: "200000" } +``` + +## Automatic Gas Estimation + +```typescript +import { SigningStargateClient } from "@cosmjs/stargate"; + +// Auto gas estimation when sending transactions +const result = await client.signAndBroadcast( + sender, + messages, + "auto" // Use auto gas estimation +); +``` + +## Gas Estimation via Simulation + +```typescript +import { SigningStargateClient } from "@cosmjs/stargate"; + +// First simulate the transaction +const gasEstimate = await client.simulate( + sender, + messages, + memo +); + +// Apply a safety margin (e.g., 1.3x the estimated gas) +const gasLimit = Math.round(gasEstimate * 1.3); + +// Calculate fee with the estimated gas +const fee = calculateFee(gasLimit, gasPrice); ``` -if you are broadcasting multiple messages in a batch, you should `simulate` your tx and estimate the fee +## Fee Adjustment Strategies -```js -import { Dec, IntPretty } from '@keplr-wallet/unit'; +| Strategy | Description | Use Case | +| -------- | ----------- | -------- | +| Fixed Multiplier | Multiply estimated gas by a factor | General usage (1.3-1.5x typical) | +| Minimum Gas | Set a floor for gas estimation | Ensure transaction validity | +| Dynamic Pricing | Adjust gas price based on network congestion | Prioritize during high traffic | -const gasEstimated = await stargateClient.simulate(address, msgs, memo); +## Fee Estimation with Telescope-generated Clients + +```typescript +import { createTxClient } from "./tx"; + +const txClient = await createTxClient({ + signer: wallet, + rpcEndpoint: "https://rpc.cosmos.network" +}); + +// Simulate to estimate gas +const gasEstimate = await txClient.simulate({ + messages, + signer: sender +}); + +// Apply safety margin and calculate fee +const gasLimit = Math.round(gasEstimate * 1.3); const fee = { - amount: coins(0, 'uosmo'), - gas: new IntPretty(new Dec(gasEstimated).mul(new Dec(1.3))) - .maxDecimals(0) - .locale(false) - .toString() + amount: [{ denom: "uatom", amount: (gasLimit * 0.025).toString() }], + gas: gasLimit.toString() }; ``` + +## Fee Denom Selection + +```typescript +// Get available account balances +const { balances } = await queryClient.cosmos.bank.v1beta1.allBalances({ + address: sender +}); + +// Select appropriate denom for fees +function selectFeeDenom(balances, preferredDenom = "uatom") { + // First try preferred denom + const preferred = balances.find(coin => coin.denom === preferredDenom); + if (preferred && parseInt(preferred.amount) > 5000) { + return preferredDenom; + } + + // Fallback to first denom with sufficient balance + const fallback = balances.find(coin => parseInt(coin.amount) > 5000); + return fallback ? fallback.denom : preferredDenom; +} + +const feeDenom = selectFeeDenom(balances); +``` + +## Fee Grants + +Some chains support fee grants, allowing one account to pay fees for another: + +```typescript +import { MsgGrantAllowance } from "./cosmos/feegrant/v1beta1/tx"; +import { BasicAllowance } from "./cosmos/feegrant/v1beta1/feegrant"; + +// Create a basic allowance +const expirationDate = new Date(); +expirationDate.setMonth(expirationDate.getMonth() + 1); // 1 month expiry + +const basicAllowance = BasicAllowance.fromPartial({ + spendLimit: [{ denom: "uatom", amount: "1000000" }], + expiration: expirationDate +}); + +// Grant fee allowance +const grantMsg = MsgGrantAllowance.fromPartial({ + granter: "cosmos1granter...", + grantee: "cosmos1grantee...", + allowance: Any.pack(basicAllowance, "/cosmos.feegrant.v1beta1.BasicAllowance") +}); +``` + +## Chain-Specific Fee Parameters + +| Parameter | Value Source | Example | +| --------- | ----------- | ------- | +| Minimum Gas Prices | Chain configuration | "0.0025uatom" | +| Fee Denominations | Chain-accepted tokens | ["uatom", "uosmo", ...] | +| Gas Adjustment | Client configuration | 1.3 | + +## Gas Weights for Common Operations + +| Operation | Approximate Gas Cost | Notes | +| --------- | -------------------- | ----- | +| Token Transfer | 80,000 - 100,000 | Single MsgSend | +| Delegate | 140,000 - 180,000 | MsgDelegate | +| Undelegate | 150,000 - 190,000 | MsgUndelegate | +| Claim Rewards | 150,000 - 250,000 | MsgWithdrawDelegatorReward | +| Submit Proposal | 200,000+ | Depends on proposal content size | +| Vote on Proposal | 100,000 - 140,000 | MsgVote | + +## Handling Out of Gas Errors + +```typescript +try { + const result = await client.signAndBroadcast(sender, messages, fee); + // Process result +} catch (error) { + if (error.message.includes("out of gas")) { + // Increase gas and retry + const newGasLimit = Math.round(parseInt(fee.gas) * 1.5); + const newFee = { + amount: fee.amount, + gas: newGasLimit.toString() + }; + // Retry with new fee + const retryResult = await client.signAndBroadcast(sender, messages, newFee); + // Process retry result + } else { + // Handle other errors + console.error("Transaction failed:", error); + } +} +``` + +## Best Practices + +1. Always use gas estimation for complex transactions +2. Apply a safety margin of 1.3x to 1.5x to estimated gas +3. Set a reasonable maximum gas limit to avoid excessive fees +4. Check account balances before sending to ensure sufficient funds for fees +5. Consider network congestion and adjust gas prices accordingly +6. Use appropriate denominations accepted by validators +7. Test fee calculations on testnets before mainnet deployment \ No newline at end of file diff --git a/docs/composing-messages.mdx b/docs/composing-messages.mdx index 607983eb63..a93bad57f1 100644 --- a/docs/composing-messages.mdx +++ b/docs/composing-messages.mdx @@ -1,33 +1,262 @@ -## Composing Messages +# Composing Messages -This example shows messages from the `osmojs`, which was built with Telescope. +This document provides a reference for composing messages using Telescope-generated types to interact with Cosmos SDK blockchains. -Import the `osmosis` object from `osmojs`. In this case, we're show the messages available from the `osmosis.gamm.v1beta1` module: +## Message Structure -```js -import { osmosis } from 'osmojs'; +| Component | Description | +| --------- | ----------- | +| Message Type | Protocol Buffer message type with corresponding TypeScript interface | +| Message Content | Data conforming to the message structure | +| Encoding | Binary or JSON encoding of the message | +| Signing | Cryptographic signature of the message for on-chain verification | -const { - joinPool, - exitPool, - exitSwapExternAmountOut, - exitSwapShareAmountIn, - joinSwapExternAmountIn, - joinSwapShareAmountOut, - swapExactAmountIn, - swapExactAmountOut -} = osmosis.gamm.v1beta1.MessageComposer.withTypeUrl; +## Available Message Types + +Message types are typically organized by module. Common modules include: + +| Module | Description | Examples | +| ------ | ----------- | -------- | +| `bank` | Token transfers | `MsgSend`, `MsgMultiSend` | +| `staking` | Staking and delegation | `MsgDelegate`, `MsgUndelegate` | +| `gov` | Governance | `MsgSubmitProposal`, `MsgVote` | +| `distribution` | Fee distribution | `MsgWithdrawDelegatorReward` | +| `authz` | Authorization | `MsgExec`, `MsgGrant` | + +## Creating Messages + +Messages can be created using the `.fromPartial()` method: + +```typescript +import { MsgSend } from "./cosmos/bank/v1beta1/tx"; + +// Create a bank send message +const sendMsg = MsgSend.fromPartial({ + fromAddress: "cosmos1...", + toAddress: "cosmos1...", + amount: [ + { denom: "uatom", amount: "1000000" } + ] +}); +``` + +### Message Validation + +Telescope-generated types perform runtime validation when using helper methods: + +```typescript +// Will throw an error if invalid +const validatedMsg = MsgSend.fromJSON({ + from_address: "cosmos1...", + to_address: "cosmos1...", + amount: [ + { denom: "uatom", amount: "1000000" } + ] +}); +``` + +## Message Composition Methods + +| Method | Description | Example | +| ------ | ----------- | ------- | +| `fromPartial` | Creates a message with default values for missing fields | `MsgSend.fromPartial({...})` | +| `fromJSON` | Creates a message from a JSON object | `MsgSend.fromJSON({...})` | +| `encode` | Encodes a message to binary | `MsgSend.encode(msg, writer)` | +| `decode` | Decodes a binary message | `MsgSend.decode(bytes)` | + +## MessageComposer + +For convenience, Telescope generates a `MessageComposer` class for each module: + +```typescript +import { bankComposer } from "./cosmos/bank/v1beta1/tx.composer"; + +const composedSendMsg = bankComposer.send({ + fromAddress: "cosmos1...", + toAddress: "cosmos1...", + amount: [{ denom: "uatom", amount: "1000000" }] +}); +``` + +### MessageComposer Methods + +Each MessageComposer provides: + +| Property | Description | +| -------- | ----------- | +| `typeUrl` | The type URL for the message | +| `value` | The message content | + +## Transaction Composition + +Multiple messages can be combined in a single transaction: + +```typescript +// Compose multiple messages +const messages = [ + bankComposer.send({ + fromAddress: "cosmos1...", + toAddress: "cosmos1...", + amount: [{ denom: "uatom", amount: "1000000" }] + }), + stakingComposer.delegate({ + delegatorAddress: "cosmos1...", + validatorAddress: "cosmosvaloper1...", + amount: { denom: "uatom", amount: "5000000" } + }) +]; +``` + +## Protobuf vs. Amino Encoding + +Telescope supports both Protobuf and Amino encoding formats: + +| Format | Usage | Type Field | +| ------ | ----- | ---------- | +| Protobuf | Modern Cosmos SDKs | `typeUrl` (e.g., "/cosmos.bank.v1beta1.MsgSend") | +| Amino | Legacy systems, some wallets | `type` (e.g., "cosmos-sdk/MsgSend") | + +### Amino Conversion Example + +```typescript +import { AminoTypes } from "@cosmjs/stargate"; +import { aminoConverters } from "./amino/converters"; + +const aminoTypes = new AminoTypes(aminoConverters); + +// Convert Protobuf to Amino format +const aminoMsg = aminoTypes.toAmino({ + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: sendMsg +}); + +// Convert back to Protobuf format +const protoMsg = aminoTypes.fromAmino(aminoMsg); +``` + +## Message Registry + +The registry maps message type URLs to their corresponding Protobuf types: + +```typescript +import { registry } from "./registry"; +import { Registry } from "@cosmjs/proto-signing"; + +const protoRegistry = new Registry(); +registry.forEach(([typeUrl, type]) => { + protoRegistry.register(typeUrl, type); +}); +``` + +## Using Messages with Clients + +### With Stargate Client + +```typescript +import { SigningStargateClient } from "@cosmjs/stargate"; +import { MsgSend } from "./cosmos/bank/v1beta1/tx"; + +// Create a message +const sendMsg = MsgSend.fromPartial({ + fromAddress: "cosmos1...", + toAddress: "cosmos1...", + amount: [{ denom: "uatom", amount: "1000000" }] +}); + +// Send transaction with message +const result = await signingClient.signAndBroadcast( + "cosmos1...", + [ + { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: sendMsg + } + ], + fee +); +``` + +### With RPC Client + +```typescript +import { createTxClient } from "./tx"; + +const txClient = await createTxClient({ + signer: wallet, + rpcEndpoint: "https://rpc.cosmos.network" +}); + +const result = await txClient.signAndBroadcast( + "cosmos1...", + [sendMsg], + fee +); +``` + +## Common Message Types + +### Bank Messages + +```typescript +// MsgSend +const msgSend = MsgSend.fromPartial({ + fromAddress: "cosmos1...", + toAddress: "cosmos1...", + amount: [{ denom: "uatom", amount: "1000000" }] +}); + +// MsgMultiSend +const msgMultiSend = MsgMultiSend.fromPartial({ + inputs: [ + { address: "cosmos1...", coins: [{ denom: "uatom", amount: "1000000" }] } + ], + outputs: [ + { address: "cosmos1a...", coins: [{ denom: "uatom", amount: "500000" }] }, + { address: "cosmos1b...", coins: [{ denom: "uatom", amount: "500000" }] } + ] +}); ``` -Now you can construct messages. If you use vscode or another typescript-enabled IDE, you should also be able to use `ctrl+space` to see auto-completion of the fields required for the message. +### Staking Messages -```js -import { coin } from '@cosmjs/amino'; +```typescript +// MsgDelegate +const msgDelegate = MsgDelegate.fromPartial({ + delegatorAddress: "cosmos1...", + validatorAddress: "cosmosvaloper1...", + amount: { denom: "uatom", amount: "1000000" } +}); -const msg = swapExactAmountIn({ - sender, - routes, - tokenIn: coin(amount, denom), - tokenOutMinAmount +// MsgUndelegate +const msgUndelegate = MsgUndelegate.fromPartial({ + delegatorAddress: "cosmos1...", + validatorAddress: "cosmosvaloper1...", + amount: { denom: "uatom", amount: "1000000" } }); ``` + +### Governance Messages + +```typescript +// MsgSubmitProposal +const msgSubmitProposal = MsgSubmitProposal.fromPartial({ + content: Any.pack(textProposal, "/cosmos.gov.v1beta1.TextProposal"), + initialDeposit: [{ denom: "uatom", amount: "10000000" }], + proposer: "cosmos1..." +}); + +// MsgVote +const msgVote = MsgVote.fromPartial({ + proposalId: 1, + voter: "cosmos1...", + option: VoteOption.VOTE_OPTION_YES +}); +``` + +## Best Practices + +1. Always use the `.fromPartial()` method to ensure default values are properly set +2. Keep type URLs consistent with the SDK version you're targeting +3. Register all message types you plan to use with the registry +4. Use MessageComposer for convenience when composing multiple messages +5. Verify message structure before sending to avoid on-chain errors \ No newline at end of file diff --git a/docs/cosmwasm.mdx b/docs/cosmwasm.mdx index 9520d5366c..d9bb42a623 100644 --- a/docs/cosmwasm.mdx +++ b/docs/cosmwasm.mdx @@ -1,22 +1,221 @@ -## CosmWasm +# CosmWasm Integration -Generate TypeScript SDKs for your CosmWasm smart contracts by using the `cosmwasm` option on `TelescopeOptions`. The `cosmwasm` option is actually a direct reference to the `TSBuilderInput` object, for the most up-to-date documentation, visit [@cosmwasm/ts-codegen](https://github.com/CosmWasm/ts-codegen). +This document provides a reference for generating TypeScript SDKs for CosmWasm smart contracts using Telescope. + +## Overview + +Telescope integrates with [@cosmwasm/ts-codegen](https://github.com/CosmWasm/ts-codegen) to generate TypeScript client libraries for CosmWasm smart contracts. This enables you to work with your smart contracts using strongly-typed interfaces in your frontend applications. + +## Configuration + +To generate TypeScript SDKs for your CosmWasm contracts, add the `cosmwasm` option to your Telescope configuration: + +```typescript +import { TelescopeOptions } from "@cosmology/types"; -```ts -import { TSBuilderInput } from '@cosmwasm/ts-codegen'; const options: TelescopeOptions = { cosmwasm: { contracts: [ { - name: 'SG721', - dir: './path/to/sg721/schema' - }, - { - name: 'Minter', - dir: './path/to/Minter/schema' + name: "MyContract", + dir: "./path/to/schema" } ], - outPath: './path/to/code/src/' + outPath: "./src/contracts" } }; ``` + +The `cosmwasm` option is a direct reference to the `TSBuilderInput` object from the `@cosmwasm/ts-codegen` package. + +## Contract Configuration + +### Basic Contract Setup + +At minimum, each contract configuration requires: + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `name` | string | Name of the contract, used in generated class names | +| `dir` | string | Path to the contract schema directory | + +Example: + +```typescript +{ + name: "SG721", + dir: "./schema/sg721" +} +``` + +### Advanced Contract Configuration + +For more control, you can use additional configuration options: + +```typescript +{ + name: "Marketplace", + dir: "./schema/marketplace", + // Additional options + camelCase: true, + customTypes: { + "cosmos.base.v1beta1.Coin": { + module: "@cosmjs/stargate", + type: "Coin" + } + }, + messageComposer: { + enabled: true + } +} +``` + +## Output Configuration + +The `outPath` property specifies where the generated files should be placed: + +```typescript +{ + outPath: "./src/generated/contracts" +} +``` + +## Full Configuration Options + +| Property | Type | Description | Default | +| -------- | ---- | ----------- | ------- | +| `contracts` | array | List of contract configurations | Required | +| `contracts[].name` | string | Contract name | Required | +| `contracts[].dir` | string | Path to contract schema | Required | +| `contracts[].camelCase` | boolean | Convert snake_case to camelCase | `true` | +| `contracts[].customTypes` | object | Custom type mappings | `{}` | +| `contracts[].messageComposer` | object | Message composer options | `{ enabled: true }` | +| `contracts[].types` | object | Custom types generation options | `{ enabled: true }` | +| `contracts[].client` | object | Client generation options | `{ enabled: true }` | +| `contracts[].reactQuery` | object | React Query hooks options | `{ enabled: false, version: 'v4' }` | +| `contracts[].recoil` | object | Recoil state management options | `{ enabled: false }` | +| `contracts[].bundle` | object | Bundle generation options | `{ enabled: false }` | +| `outPath` | string | Output directory for generated files | Current directory | + +## Generated Files + +For each contract, the following files are generated: + +| File | Description | +| ---- | ----------- | +| `.types.ts` | TypeScript interfaces for contract interactions | +| `.client.ts` | Client classes for interacting with the contract | +| `.message-composer.ts` | Helper for composing contract messages | +| `.react-query.ts` | React Query hooks (if enabled) | +| `.recoil.ts` | Recoil state atoms and selectors (if enabled) | + +## Using Generated Clients + +The generated clients provide a type-safe way to interact with your CosmWasm contracts: + +```typescript +import { MyContractClient } from "./generated/contracts/MyContract.client"; +import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"; + +// Create a client instance +const client = new MyContractClient( + signingCosmWasmClient, + "cosmos1...", // Contract address + {} // Default options +); + +// Query contract state +const { balance } = await client.getBalance({ address: "cosmos1..." }); + +// Execute contract functions +const result = await client.mint({ + tokenId: "123", + owner: "cosmos1..." +}); +``` + +## Message Composer + +The generated message composer helps create contract messages in the correct format: + +```typescript +import { MyContractMessageComposer } from "./generated/contracts/MyContract.message-composer"; + +const msgComposer = new MyContractMessageComposer("cosmos1..."); // Contract address + +// Create a message for broadcasting +const mintMsg = msgComposer.mint({ + tokenId: "123", + owner: "cosmos1..." +}); + +// Use with a SigningCosmWasmClient +const result = await signingClient.signAndBroadcast( + senderAddress, + [mintMsg], + fee +); +``` + +## React Query Integration + +If you enable React Query integration, you'll get custom hooks for each query and mutation: + +```typescript +import { useMyContractGetBalance } from "./generated/contracts/MyContract.react-query"; + +function BalanceDisplay({ address }) { + const { data, isLoading, error } = useMyContractGetBalance({ + address + }); + + if (isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return
Balance: {data.balance}
; +} +``` + +## Customizing Type Imports + +You can customize how external types are imported using the `customTypes` configuration: + +```typescript +customTypes: { + "cosmos.base.v1beta1.Coin": { + module: "@cosmjs/stargate", + type: "Coin" + }, + "SomeCustomType": { + module: "../types", + type: "MyType" + } +} +``` + +## Dependencies + +To use the generated code, you'll need to install the following dependencies: + +```bash +npm install @cosmjs/cosmwasm-stargate @cosmjs/stargate +``` + +For React Query integration: + +```bash +npm install @tanstack/react-query +``` + +For Recoil integration: + +```bash +npm install recoil +``` + +## Limitations + +- Schema files must follow the CosmWasm JSON Schema format +- Custom integration might be needed for very complex contract architectures +- Generated code relies on the runtime libraries specified in dependencies +``` \ No newline at end of file diff --git a/docs/creating-signers.mdx b/docs/creating-signers.mdx new file mode 100644 index 0000000000..a1ac8b014c --- /dev/null +++ b/docs/creating-signers.mdx @@ -0,0 +1,186 @@ +# Creating Signers + +This document provides a reference for creating and using signers in Telescope generated clients. + +## Overview + +To broadcast transactions to Cosmos-based blockchains, you need to sign them with a valid account. Telescope supports multiple signer implementations that work with its generated clients. + +## Signer Types + +### OfflineSigner + +The `OfflineSigner` interface from `@cosmjs/proto-signing` is the base interface for all signers in Telescope. It provides the following methods: + +- `getAccounts()`: Returns the accounts controlled by this signer +- `signDirect(signerAddress, signDoc)`: Signs a transaction using the direct signing method + +### DirectSecp256k1Wallet + +A wallet implementation that uses the Secp256k1 elliptic curve for signing. + +```ts +import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing'; + +// Create a wallet from a private key +const wallet = await DirectSecp256k1Wallet.fromKey(privateKey, prefix); + +// Create a random wallet +const wallet = await DirectSecp256k1Wallet.generate(prefix); +``` + +### Amino Signer vs Proto Signer + +Telescope supports both Amino and Protobuf transaction signing: + +#### Amino Signer + +Most applications should use the Amino signer for compatibility with wallets like Keplr: + +```ts +import { getOfflineSigner as getOfflineSignerAmino } from 'cosmjs-utils'; + +const signer = await getOfflineSignerAmino({ + mnemonic, + chain +}); +``` + +#### Proto Signer + +For direct Protobuf signing: + +```ts +import { getOfflineSigner as getOfflineSignerProto } from 'cosmjs-utils'; + +const signer = await getOfflineSignerProto({ + mnemonic, + chain +}); +``` + +## Wallet Integration + +### Keplr Wallet + +Telescope clients integrate well with the Keplr browser extension: + +```ts +// Request connection to Keplr +await window.keplr.enable(chainId); + +// Get the offline signer from Keplr +const signer = window.keplr.getOfflineSigner(chainId); + +// Use with your Telescope client +const client = await getSigningClient({ + rpcEndpoint, + signer +}); +``` + +### Using Mnemonics (Development Only) + +You can create signers from mnemonics for development purposes: + +```ts +import { chains } from 'chain-registry'; + +const mnemonic = 'unfold client turtle either pilot stock floor glow toward bullet car science'; +const chain = chains.find(({ chain_name }) => chain_name === 'osmosis'); +const signer = await getOfflineSignerAmino({ + mnemonic, + chain +}); +``` + +> **WARNING**: Never use plain-text mnemonics in production. Always use secure key management practices. + +## Custom Registry Configuration + +When creating signers for custom chains, you may need to configure the registry and amino types: + +```ts +import { Registry } from '@cosmjs/proto-signing'; +import { AminoTypes } from '@cosmjs/stargate'; +import { + cosmosAminoConverters, cosmosProtoRegistry, + cosmwasmAminoConverters, cosmwasmProtoRegistry, + ibcAminoConverters, ibcProtoRegistry, + myChainAminoConverters, myChainProtoRegistry +} from 'my-chain-js'; + +const protoRegistry = [ + ...cosmosProtoRegistry, + ...cosmwasmProtoRegistry, + ...ibcProtoRegistry, + ...myChainProtoRegistry +]; + +const aminoConverters = { + ...cosmosAminoConverters, + ...cosmwasmAminoConverters, + ...ibcAminoConverters, + ...myChainAminoConverters +}; + +const registry = new Registry(protoRegistry); +const aminoTypes = new AminoTypes(aminoConverters); + +const stargateClient = await SigningStargateClient.connectWithSigner( + rpcEndpoint, + signer, + { + registry, + aminoTypes + } +); +``` + +## Key Management Best Practices + +When working with signers in production applications: + +1. **Never store private keys or mnemonics in client-side code** +2. **Use hardware wallets or browser extensions when possible** +3. **Consider using secure enclave or TPM storage for server-side applications** +4. **Implement proper key rotation and revocation procedures** +5. **Use AES encryption for any stored key material** + +## Common Issues and Solutions + +### Chain Prefix Mismatch + +If you encounter address verification errors, ensure you're using the correct chain prefix: + +```ts +// For Cosmos Hub +const wallet = await DirectSecp256k1Wallet.generate('cosmos'); + +// For Osmosis +const wallet = await DirectSecp256k1Wallet.generate('osmo'); +``` + +### Transaction Simulation + +Before broadcasting transactions, it's recommended to simulate them first: + +```ts +// Simulate the transaction to estimate gas +const gasEstimated = await stargateClient.simulate(address, msgs, memo); + +// Add buffer for safety +const gasWithBuffer = Math.round(gasEstimated * 1.3); + +// Create fee with estimated gas +const fee = { + amount: coins(0, 'uatom'), + gas: gasWithBuffer.toString() +}; +``` + +## Related Documentation + +- [Stargate Clients](./stargate-clients.md) +- [Composing Messages](./composing-messages.md) +- [Calculating Fees](./calculating-fees.md) \ No newline at end of file diff --git a/docs/dependencies.mdx b/docs/dependencies.mdx index f4bbbb5f4f..43e99c8e2c 100644 --- a/docs/dependencies.mdx +++ b/docs/dependencies.mdx @@ -1,20 +1,140 @@ -## Dependencies +# Dependencies -If you don't use the boilerplate, you will need to manually install +This document provides a reference for the required dependencies when using Telescope-generated code. -* `@cosmjs/amino` -* `@cosmjs/proto-signing` -* `@cosmjs/stargate` -* `@cosmjs/tendermint-rpc` +## Required Dependencies + +When using code generated by Telescope, you need to install several CosmJS packages as dependencies: ```sh yarn add @cosmjs/amino @cosmjs/proto-signing @cosmjs/stargate @cosmjs/tendermint-rpc ``` -If you use the LCD Client generation, you'll need to add +Or using npm: + +```sh +npm install @cosmjs/amino @cosmjs/proto-signing @cosmjs/stargate @cosmjs/tendermint-rpc +``` + +## Core Dependencies + +| Package | Description | Required For | +| ------- | ----------- | ------------ | +| `@cosmjs/amino` | Implements the Amino encoding format | Amino encoding support, transaction signing | +| `@cosmjs/proto-signing` | Provides Protobuf-based transaction signing | Direct signing, registry management | +| `@cosmjs/stargate` | High-level client for interacting with Cosmos SDK chains | Stargate client functionality | +| `@cosmjs/tendermint-rpc` | Tendermint RPC client implementation | Lower-level blockchain interaction | + +## Optional Dependencies -* `@cosmology/lcd` +Depending on which Telescope features you're using, you may need additional dependencies: + +### LCD Clients + +If you're using the LCD Client functionality: ```sh yarn add @cosmology/lcd ``` + +### React Query Integration + +If you're using the React Query integration: + +```sh +yarn add @tanstack/react-query +``` + +### Vue Query Integration + +If you're using the Vue Query integration: + +```sh +yarn add @tanstack/vue-query +``` + +### Recoil Integration + +If you're using the Recoil state management: + +```sh +yarn add recoil +``` + +### Pinia Integration + +If you're using the Pinia state management: + +```sh +yarn add pinia +``` + +### Mobx Integration + +If you're using the Mobx state management: + +```sh +yarn add mobx mobx-react +``` + +### CosmWasm Integration + +If you're working with CosmWasm contracts: + +```sh +yarn add @cosmjs/cosmwasm-stargate +``` + +## Peer Dependencies + +The following packages are often used alongside Telescope-generated code but are not strictly required: + +| Package | Description | +| ------- | ----------- | +| `@cosmjs/crypto` | Cryptographic primitives | +| `@cosmjs/encoding` | Encoding utilities | +| `@cosmjs/math` | Mathematical utilities, including Decimal type | +| `@cosmjs/utils` | General utilities | + +## Development Dependencies + +When setting up a new project with Telescope, you'll need: + +```sh +yarn add --dev @cosmology/telescope +``` + +## Version Compatibility + +Ensure that your CosmJS packages have compatible versions. Typically, you should install the same version for all CosmJS packages. + +Current recommended CosmJS version: + +```sh +yarn add @cosmjs/amino@0.31.1 @cosmjs/proto-signing@0.31.1 @cosmjs/stargate@0.31.1 @cosmjs/tendermint-rpc@0.31.1 +``` + +## Example package.json + +Here's an example `package.json` dependencies section for a project using Telescope-generated code with React: + +```json +{ + "dependencies": { + "@cosmjs/amino": "^0.31.1", + "@cosmjs/proto-signing": "^0.31.1", + "@cosmjs/stargate": "^0.31.1", + "@cosmjs/tendermint-rpc": "^0.31.1", + "@cosmology/lcd": "^0.12.0", + "@tanstack/react-query": "^4.29.5", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@cosmology/telescope": "^1.0.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "typescript": "^5.0.4" + } +} +``` \ No newline at end of file diff --git a/docs/developing.mdx b/docs/developing.mdx new file mode 100644 index 0000000000..8d7fcaf295 --- /dev/null +++ b/docs/developing.mdx @@ -0,0 +1,241 @@ +# Developing + +This document provides guidance for contributing to the Telescope project and developing with Telescope-generated code. + +## Setting Up the Development Environment + +### Prerequisites + +Before you begin, ensure you have the following installed: + +- Node.js (v14 or later) +- Yarn (v1.x) +- Git + +### Clone the Repository + +```sh +git clone https://github.com/hyperweb-io/telescope.git +cd telescope +``` + +### Install Dependencies + +```sh +yarn install +``` + +### Build Packages + +Build all packages in the monorepo: + +```sh +yarn build +``` + +### Run Tests + +```sh +yarn test +``` + +## Project Structure + +Telescope is organized as a monorepo with several packages: + +| Package | Description | +| ------- | ----------- | +| `packages/telescope` | Main Telescope code generator package | +| `packages/types` | TypeScript type definitions | +| `packages/ast` | Abstract syntax tree utilities | +| `packages/parser` | Protobuf parsing utilities | +| `packages/utils` | Shared utility functions | +| `packages/lcd` | LCD client utilities | +| `packages/starship` | Testing and development utilities | + +## Making Changes + +### Development Workflow + +1. Create a new branch for your changes: + +```sh +git checkout -b feature/your-feature-name +``` + +2. Make your changes to the codebase +3. Add tests for your changes +4. Run the tests to ensure they pass: + +```sh +yarn test +``` + +5. Build the packages to ensure they compile correctly: + +```sh +yarn build +``` + +6. Commit your changes with a descriptive commit message: + +```sh +git commit -m "feat: add new feature" +``` + +7. Push your branch to GitHub: + +```sh +git push origin feature/your-feature-name +``` + +8. Create a pull request on GitHub + +### Commit Message Convention + +Telescope follows the [Conventional Commits](https://www.conventionalcommits.org/) specification for commit messages: + +- `feat`: A new feature +- `fix`: A bug fix +- `docs`: Documentation changes +- `style`: Code style changes (formatting, etc.) +- `refactor`: Code changes that neither fix a bug nor add a feature +- `perf`: Performance improvements +- `test`: Adding or fixing tests +- `chore`: Changes to the build process or auxiliary tools + +Example: + +``` +feat(parser): add support for custom annotations +``` + +## Testing + +### Running Tests + +Run all tests: + +```sh +yarn test +``` + +Run tests for a specific package: + +```sh +yarn workspace @cosmology/telescope test +``` + +Run a specific test: + +```sh +yarn test -t "test name" +``` + +### Adding Tests + +When adding new features or fixing bugs, you should also add tests to ensure the functionality works as expected. Tests are located in the `__tests__` directory of each package. + +Example test: + +```typescript +import { parseProto } from '../src/parser'; + +describe('Parser', () => { + it('should parse a simple proto file', () => { + const proto = ` + syntax = "proto3"; + package example; + message Test { + string name = 1; + } + `; + + const result = parseProto(proto); + expect(result.package).toBe('example'); + expect(result.messages.length).toBe(1); + expect(result.messages[0].name).toBe('Test'); + }); +}); +``` + +## Debugging + +### Debugging Telescope + +If you're developing Telescope itself, you can add debug logging to help understand what's happening: + +```typescript +import { logger } from '@cosmology/utils'; + +logger.debug('Some debug information', { data }); +``` + +Set the log level to debug: + +```typescript +import { setLogLevel } from '@cosmology/utils'; + +setLogLevel('debug'); +``` + +### Debugging Generated Code + +If you're developing with Telescope-generated code, you can use the standard TypeScript debugging tools, such as: + +- VS Code debugging +- Chrome DevTools with source maps +- `console.log` statements (temporarily) + +## Creating a New Release + +### Versioning + +Telescope follows [Semantic Versioning](https://semver.org/): + +- MAJOR version for incompatible API changes +- MINOR version for adding functionality in a backward-compatible manner +- PATCH version for backward-compatible bug fixes + +### Release Process + +1. Update the version numbers in package.json files +2. Update the CHANGELOG.md files +3. Create a new release with a tag matching the version number +4. Publish to npm + +```sh +yarn publish +``` + +## Best Practices + +### Code Style + +- Use TypeScript for type safety +- Follow the existing code style in the codebase +- Use prettier for code formatting +- Add JSDoc comments for public APIs + +### Pull Requests + +- Keep PRs focused on a single topic +- Add sufficient description of the changes +- Reference related issues +- Ensure all tests pass +- Update documentation as needed + +### Documentation + +When making changes, update the relevant documentation: + +- Update READMEs if necessary +- Add JSDoc comments to new functions +- Update examples if API changes are made + +## Additional Resources + +- [Telescope GitHub Repository](https://github.com/hyperweb-io/telescope) +- [Cosmos SDK Documentation](https://docs.cosmos.network/) +- [CosmJS Documentation](https://cosmos.github.io/cosmjs/) +- [Protobuf Documentation](https://developers.google.com/protocol-buffers/docs/overview) \ No newline at end of file diff --git a/docs/get-started.mdx b/docs/get-started.mdx deleted file mode 100644 index b085548550..0000000000 --- a/docs/get-started.mdx +++ /dev/null @@ -1,96 +0,0 @@ -## Quickstart - -Follow the instructions below to generate a new Typescript package that you can publish to npm. - -First, install `telescope`: - -```sh -npm install -g @cosmology/telescope -``` - -### Generate - -Use the `generate` command to create a new package. - -```sh -telescope generate -cd ./your-new-project -yarn -``` - -### Add Protobufs - -If you have `.proto` files, simply add them to a `./proto` folder. - -However, if you want to get started quickly using existing protos from our registry, simply use the `install` command. - -```sh -telescope install -``` - -It's not necessary, but you may also specify specific packages, e.g. - -```sh -telescope install @protobufs/osmosis -``` - -### Transpile - -To create the Typescript files, run the `transpile` command. - -```sh -telescope transpile -``` - -You should now see some `.ts` files generated in `./src`. These are the real source files used in your application. - -Examples: - -```sh -# Telescope takes chain1 folder as input, -# and generate files in 'gen/src' folder. -telescope transpile --protoDirs ../../__fixtures__/chain1 --outPath gen/src -``` - -```sh -# Telescope takes chain1 folder as input, -# and generate files in 'gen/src' folder using default telescope options. -telescope transpile --protoDirs ../../__fixtures__/chain1 --outPath gen/src --useDefaults -``` - -```sh -# Telescope takes chain1 folder(from args) and chain2 folder(from config) as input, -# and generate files in 'gen/src'(defined in the config file, will override outPath in args) folder using a config file. -# Note: --config will override --useDefaults. -telescope transpile --protoDirs ../../__fixtures__/chain1 --config .telescope.json -``` - -```sh -# Telescope takes more than one config. The config afterward will override those in front. In this case values in .telescope-ext.json will override those in .telescope.json. -telescope transpile --config .telescope.json --config .telescope-ext.json -``` - -```json -//.telescope.json -{ - "protoDirs": [ - "../../fixtures/chain2" - ], - "outPath": "gen/src", - "options": { - // telescope options - ... - } -} -``` - -### Build - -Finally, run `install` and `buidl` to generate the JS and types for publishing your module to npm. - -```sh -yarn install -yarn buidl -``` - -Now you should have code inside of your `./src` folder, ready for publishing via `npm publish`. Or, if you used the defaults, you can start developing and your code can be imported from `./src/codegen`; diff --git a/docs/helper-functions-configuration.mdx b/docs/helper-functions-configuration.mdx new file mode 100644 index 0000000000..7ad8c98cb9 --- /dev/null +++ b/docs/helper-functions-configuration.mdx @@ -0,0 +1,164 @@ +# Helper Functions Configuration + +This document provides a reference for configuring helper function generation in Telescope. + +## Overview + +Helper functions provide a simplified interface to interact with your blockchain's services. Telescope can automatically generate these functions based on your proto definitions, allowing for cleaner and more intuitive code. + +## Configuration + +Helper function generation is configured in the `helperFunctions` section of your Telescope options: + +```typescript +import { TelescopeOptions } from "@cosmology/types"; + +const options: TelescopeOptions = { + helperFunctions: { + enabled: true, + hooks: { + react: true, + vue: false + }, + include: { + serviceTypes: ["Query", "Msg"], + patterns: ["cosmos.gov.v1beta1.**", "cosmos.bank.v1beta1.*Send*"] + }, + nameMappers: { + All: { + "cosmos.gov.v1beta1.*Vote*": { + funcBody: (name) => `helper${name}`, + hookPrefix: "use" + } + }, + Query: { + funcBody: (name) => `get${name}`, + hookPrefix: "use" + }, + Msg: { + funcBody: "unchanged", + hookPrefix: "useTx" + } + } + } +}; +``` + +## Configuration Properties + +### Basic Options + +| Property | Type | Description | Default | +| -------- | ---- | ----------- | ------- | +| `helperFunctions.enabled` | boolean | Enable or disable helper function generation | `false` | +| `helperFunctions.hooks` | object | Configuration for generating hook functions for React and Vue | `{ react: false, vue: false }` | + +### Include Options + +| Property | Type | Description | Default | +| -------- | ---- | ----------- | ------- | +| `helperFunctions.include.serviceTypes` | string[] | Which service types to include (`Query`, `Msg`) | `undefined` (all types) | +| `helperFunctions.include.patterns` | string[] | Glob patterns to match specific services | `undefined` (all services) | + +### Name Mapping Options + +The `nameMappers` object allows you to customize how function names are generated. It has three categories: + +1. `All` - Applied to all service types +2. `Query` - Applied only to Query services +3. `Msg` - Applied only to Msg services + +For each category, you can specify: + +| Property | Type | Description | Default | +| -------- | ---- | ----------- | ------- | +| `funcBody` | string or function | How to transform method names | Query: "get", Msg: "unchanged" | +| `hookPrefix` | string | Prefix for hook functions | "use" | + +## Pattern Matching Priority + +When multiple patterns match a service, the following priority rules apply: + +1. Service-specific patterns (`Query`, `Msg`) take precedence over `All` patterns +2. More specific patterns take precedence over general patterns +3. Patterns are case-sensitive + +## Name Transformation + +The `funcBody` property can be either: + +1. A string value: + - `"unchanged"` - Keep the original method name + - Any other string - Use as a prefix for the method name + +2. A function: `(name: string) => string` + - Receives the original method name + - Returns the transformed name + +Example function transformation: +```typescript +funcBody: (name) => `execute${name.charAt(0).toUpperCase()}${name.slice(1)}` +``` + +## Generated Output Examples + +### Basic Helper Functions + +For a service `cosmos.gov.v1beta1.Query.proposals`, with default configuration: + +```typescript +// Generated function +export function getProposals( + rpcEndpoint: string, + params: QueryProposalsRequest +): Promise { + // Implementation +} +``` + +### React Hooks + +If React hooks are enabled: + +```typescript +// Generated React hook +export function useProposals( + rpcEndpoint: string, + params: QueryProposalsRequest, + options?: UseQueryOptions +): UseQueryResult { + // Implementation +} +``` + +### Custom Naming + +With custom naming rules: + +```typescript +nameMappers: { + Query: { + "cosmos.gov.v1beta1.*Proposals*": { + funcBody: (name) => `fetch${name}`, + hookPrefix: "useGov" + } + } +} +``` + +Would generate: + +```typescript +// Generated function +export function fetchProposals(/* ... */) { /* ... */ } + +// Generated React hook +export function useGovFetchProposals(/* ... */) { /* ... */ } +``` + +## Best Practices + +1. **Limit the scope**: Use `include.patterns` to generate helpers only for the services you need +2. **Use consistent naming**: Maintain a consistent naming convention across your codebase +3. **Prefer descriptive names**: Use prefixes that describe the action (e.g., `get` for queries, `send` for transactions) +4. **Document custom mappers**: If using complex name transformations, document the logic for your team \ No newline at end of file diff --git a/docs/index.mdx b/docs/index.mdx deleted file mode 100644 index 7f7b7e6dc7..0000000000 --- a/docs/index.mdx +++ /dev/null @@ -1,175 +0,0 @@ -import { DownloadButton } from '../../components/download-button' - -# Telescope 🔭 - -

- -

- - - -

- -

- -A "babel for the Cosmos", Telescope is a TypeScript Transpiler for Cosmos Protobufs. Telescope is used to generate libraries for Cosmos blockchains. Simply point to your protobuffer files and create developer-friendly Typescript libraries for teams to build on your blockchain. - -The following blockchain libraries (generated by Telescope) are available via npm - -- [osmojs](https://www.npmjs.com/package/osmojs) -- [@dydxprotocol/v4-client-js](https://www.npmjs.com/package/@dydxprotocol/v4-client-js) -- [stargazejs](https://www.npmjs.com/package/stargazejs) -- [juno-network](https://www.npmjs.com/package/juno-network) -- [stridejs](https://www.npmjs.com/package/stridejs) -- [injectivejs](https://www.npmjs.com/package/injectivejs) -- [quicksilverjs](https://www.npmjs.com/package/quicksilverjs) - -🎥 [Checkout our video playlist](https://www.youtube.com/watch?v=n82MsLe82mk&list=PL-lMkVv7GZwyQaK6bp6kMdOS5mzosxytC) to learn how to use `telescope`! - - - -## Table of contents - -- [Telescope 🔭](#telescope) - - [Table of contents](#table-of-contents) - - [Quickstart](/telescope/get-started#quickstart) - - [Generate](/telescope/get-started#generate) - - [Add Protobufs](/telescope/get-started#add-protobufs) - - [Transpile](/telescope/get-started#transpile) - - [Build](/telescope/get-started#build) -- Usage - - [Programatic Usage](/telescope/programatic-usage) - - [Options](/telescope/options) - - [Amino Encoding](/telescope/options#amino-encoding) - - [Prototypes Options](/telescope/options#prototypes-options) - - [Prototypes Methods](/telescope/options#prototypes-methods) - - [LCD Client Options](/telescope/options#lcd-client-options) - - [RPC Client Options](/telescope/options#rpc-client-options) - - [Stargate Client Options](/telescope/options#stargate-client-options) - - [State Management](/telescope/options#state-management) - - [React Query](/telescope/options#react-query) - - [Mobx](/telescope/options#mobx) - - [Pinia](/telescope/options#pinia) - - [Typings and Formating](/telescope/options#typings-and-formating) - - [Protobuf parser](/telescope/options#protobuf-parser) - - [Typescript Disabling](/telescope/options#typescript-disabling) - - [ESLint Disabling](/telescope/options#eslint-disabling) - - [Bundle](/telescope/options#bundle) - - [Output](/telescope/options#output) - - [Types](/telescope/types) - - [Timestamp](/telescope/types#timestamp) - - [Duration](/telescope/types#duration) - - [Composing Messages](/telescope/composing-messages) - - [Calculating Fees](/telescope/calculating-fees) - - [Stargate Clients](/telescope/clients#stargate-clients) - - [Creating Signers](/telescope/clients#creating-signers) - - [Amino Signer](/telescope/clients#amino-signer) - - [Proto Signer](/telescope/clients#proto-signer) - - [Broadcasting messages](/telescope/clients#broadcasting-messages) - - [LCD Clients](/telescope/clients#lcd-clients) - - [LCD Clients Classes](/telescope/clients#lcd-clients-classes) - - [RPC Clients](/telescope/clients#rpc-clients) - - [Tendermint Client](/telescope/clients#tendermint-client) - - [gRPC-web Client](/telescope/clients#grpc-web-client) - - [gRPC-gateway Client](/telescope/clients#grpc-gateway-client) - - [RPC Client Classes](/telescope/clients#rpc-client-classes) - - [Instant RPC Methods](/telescope/clients#instant-rpc-methods) - - [Manually registering types](/telescope/manually-registering-types) - - [CosmWasm](/telescope/cosmwasm) - - [Dependencies](/telescope/dependencies) - - [Developing](/telescope/developing) - - [Sponsors](#sponsors) - - [Related](#related) - - [Credits](#credits) - - [Disclaimer](#disclaimer) - -## Sponsors - -Kudos to our sponsors: - -* [Osmosis](https://osmosis.zone) funded the creation of Telescope. - -## Related - -Checkout these related projects: - -* [@cosmology/telescope](https://github.com/hyperweb-io/telescope) Your Frontend Companion for Building with TypeScript with Cosmos SDK Modules. -* [@cosmwasm/ts-codegen](https://github.com/CosmWasm/ts-codegen) Convert your CosmWasm smart contracts into dev-friendly TypeScript classes. -* [chain-registry](https://github.com/hyperweb-io/chain-registry) Everything from token symbols, logos, and IBC denominations for all assets you want to support in your application. -* [cosmos-kit](https://github.com/hyperweb-io/cosmos-kit) Experience the convenience of connecting with a variety of web3 wallets through a single, streamlined interface. -* [create-cosmos-app](https://github.com/hyperweb-io/create-cosmos-app) Set up a modern Cosmos app by running one command. -* [interchain-ui](https://github.com/hyperweb-io/interchain-ui) The Interchain Design System, empowering developers with a flexible, easy-to-use UI kit. -* [starship](https://github.com/hyperweb-io/starship) Unified Testing and Development for the Interchain. - -## Credits - -🛠 Built by Hyperweb - -Thanks to these engineers, teams and projects for inspiring Telescope: - -* [@webmaster128](https://github.com/webmaster128) -* [@assafmo](https://github.com/assafmo) -* [osmosis-frontend](https://github.com/osmosis-labs/osmosis-frontend) -* [cosmjs](https://github.com/cosmos/cosmjs) -* [ts-proto](https://github.com/stephenh/ts-proto) -* [keplr-wallet](https://github.com/chainapsis/keplr-wallet) - -## Disclaimer - -AS DESCRIBED IN THE TELESCOPE LICENSES, THE SOFTWARE IS PROVIDED “AS IS”, AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. - -No developer or entity involved in creating Telescope will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the Telescope code or Telescope CLI, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value. - - diff --git a/docs/instant-rpc-methods.mdx b/docs/instant-rpc-methods.mdx new file mode 100644 index 0000000000..86188a2250 --- /dev/null +++ b/docs/instant-rpc-methods.mdx @@ -0,0 +1,195 @@ +# Instant RPC Methods + +This document provides a reference for using Instant RPC Methods in Telescope-generated code to simplify access to common blockchain operations. + +## Overview + +Instant RPC Methods allow you to create customized client classes that expose only specific RPC methods you need, making your code more concise and focused. Telescope generates these specialized clients in a `service-ops.ts` file in your project's root directory. + +## Configuration + +To enable Instant RPC Methods generation in Telescope: + +```typescript +import { TelescopeOptions } from "@cosmology/types"; + +const options: TelescopeOptions = { + rpcClients: { + enabled: true, + instantOps: [ + { + className: "CosmosClient", + nameMapping: { + // Optional name mapping configuration + All: { + // Methods to apply to both Query and Msg types + "cosmos.bank.v1beta1.balance": "getBalance", + "cosmos.bank.v1beta1.allBalances": "getAllBalances" + }, + Query: { + // Methods specific to Query types + "cosmos.staking.v1beta1.validators": "getValidators" + }, + Msg: { + // Methods specific to Msg types + "cosmos.bank.v1beta1.send": "sendTokens" + } + } + } + ] + } +}; +``` + +## Configuration Parameters + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `rpcClients.instantOps` | array | Array of instant RPC operations configurations | +| `rpcClients.instantOps[].className` | string | Name of the generated client class | +| `rpcClients.instantOps[].nameMapping` | object | Optional object to map method names | +| `rpcClients.instantOps[].nameMapping.All` | object | Method mapping for both Query and Msg types | +| `rpcClients.instantOps[].nameMapping.Query` | object | Method mapping specific to Query types | +| `rpcClients.instantOps[].nameMapping.Msg` | object | Method mapping specific to Msg types | + +## Basic Usage + +Once generated, the Instant RPC client can be used as follows: + +```typescript +import { CosmosClient } from "./service-ops"; +import { createRPCQueryClient } from "./codegen/rpc"; + +async function queryWithInstantClient() { + // First create a regular RPC client + const rpcClient = await createRPCQueryClient({ + rpcEndpoint: "https://rpc.cosmos.network" + }); + + // Initialize the instant client with the RPC client + const cosmosClient = new CosmosClient(); + cosmosClient.init(rpcClient); + + // Use the simplified methods + const { balance } = await cosmosClient.getBalance({ + address: "cosmos1...", + denom: "uatom" + }); + + console.log(`Balance: ${balance.amount} ${balance.denom}`); +} +``` + +## Method Mapping + +The nameMapping configuration allows you to rename methods to make them more intuitive: + +```typescript +nameMapping: { + All: { + // Original method name -> New method name + "cosmos.bank.v1beta1.balance": "getBalance", + "cosmos.bank.v1beta1.allBalances": "getAllBalances" + } +} +``` + +With this mapping, `client.cosmos.bank.v1beta1.balance()` becomes `cosmosClient.getBalance()`. + +## Creating Multiple Client Classes + +You can define multiple instant client classes, each with their own focused set of methods: + +```typescript +instantOps: [ + { + className: "BankClient", + nameMapping: { + All: { + "cosmos.bank.v1beta1.balance": "getBalance", + "cosmos.bank.v1beta1.allBalances": "getAllBalances" + } + } + }, + { + className: "StakingClient", + nameMapping: { + All: { + "cosmos.staking.v1beta1.validators": "getValidators", + "cosmos.staking.v1beta1.delegations": "getDelegations" + } + } + } +] +``` + +## Combining Query and Msg Operations + +Instant RPC clients can combine both query (read) and msg (write) operations in a single client: + +```typescript +// Configuration +nameMapping: { + Query: { + "cosmos.bank.v1beta1.balance": "getBalance" + }, + Msg: { + "cosmos.bank.v1beta1.send": "sendTokens" + } +} + +// Usage +const { balance } = await cosmosClient.getBalance({ + address: "cosmos1...", + denom: "uatom" +}); + +const sendResult = await cosmosClient.sendTokens({ + fromAddress: "cosmos1sender...", + toAddress: "cosmos1recipient...", + amount: [{ denom: "uatom", amount: "1000000" }] +}); +``` + +## Benefits of Instant RPC Methods + +1. **Simplified Interface**: Access methods directly without navigating through module hierarchies +2. **Intuitive Naming**: Use custom method names that better describe their purpose +3. **Focused API**: Include only the methods your application needs +4. **Improved Maintainability**: Cleaner code with less nesting and shorter method calls +5. **Better Developer Experience**: More intuitive API for team members + +## Generated Code Structure + +The generated `service-ops.ts` file contains: + +1. An interface with all included operations +2. A class that implements the interface +3. An initialization method that links to the underlying RPC client + +```typescript +// Generated example (simplified) +export interface CosmosClient extends + _CosmosAuthV1beta1Queryrpc.CosmosAuthAccountQuery, + _CosmosBankV1beta1Queryrpc.CosmosBankBalanceQuery { +} + +export class CosmosClient { + rpc: Rpc; + + init(rpc: Rpc): void { + this.rpc = rpc; + this.getBalance = _CosmosBankV1beta1Queryrpc.createClientImpl(rpc).balance; + this.getAllBalances = _CosmosBankV1beta1Queryrpc.createClientImpl(rpc).allBalances; + // Additional method assignments... + } +} +``` + +## Best Practices + +1. **Use Meaningful Names**: Choose method names that clearly describe what the operation does +2. **Group Related Operations**: Create separate client classes for different functional areas +3. **Maintain Consistency**: Use consistent naming patterns across your custom methods +4. **Document Custom Methods**: Add comments to explain your custom methods if their purpose isn't obvious +5. **Use with TypeScript**: Leverage type safety to catch errors at compile time \ No newline at end of file diff --git a/docs/json-patch-protos.mdx b/docs/json-patch-protos.mdx new file mode 100644 index 0000000000..d11fe9fc6d --- /dev/null +++ b/docs/json-patch-protos.mdx @@ -0,0 +1,138 @@ +# JSON Patch Protos + +This document provides a reference for using JSON Patch Protos feature to modify protocol buffer definitions without changing the original files. + +## Overview + +JSON Patch Protos allows you to apply modifications to protobuf definitions during code generation. This feature is particularly useful when you need to customize the generated code without altering the original proto files, such as when upstream SDK PRs are delayed or not in production. + +## Configuration + +JSON Patch Protos is configured in the `prototypes.patch` section of your Telescope options: + +```typescript +import { TelescopeOptions } from "@cosmology/types"; + +const options: TelescopeOptions = { + prototypes: { + patch: { + "cosmwasm/wasm/v1/types.proto": [ + { + op: "replace", + path: "@/AccessType/valuesOptions/ACCESS_TYPE_UNSPECIFIED/(gogoproto.enumvalue_customname)", + value: "UnspecifiedAccess" + }, + // more operations... + ], + // more files... + } + } +}; +``` + +## Operation Structure + +Each patch operation consists of: + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `op` | string | The operation type (`add` or `replace`) | +| `path` | string | The JSON path to the target field (may use `@` prefix) | +| `value` | any | The new value to set at the target location | + +## Path Specification + +Paths can be specified in two formats: + +1. **Absolute paths**: Direct JSON paths to the field you want to modify. +2. **Package-derived paths**: Paths prefixed with `@` will be automatically derived from the package name, making navigation within the proto file's structure simpler. + +## Operation Types + +### Replace Operation + +The `replace` operation updates an existing value: + +```json +{ + "op": "replace", + "path": "@/AccessType/valuesOptions/ACCESS_TYPE_UNSPECIFIED/(gogoproto.enumvalue_customname)", + "value": "UnspecifiedAccess" +} +``` + +### Add Operation + +The `add` operation creates a new value or adds to an existing one: + +```json +{ + "op": "add", + "path": "@/AccessType/values/ACCESS_TYPE_SUPER_FUN", + "value": 4 +} +``` + +## Common Use Cases + +### Modifying Enum Values + +Customize enum values and their properties: + +```json +[ + { + "op": "replace", + "path": "@/AccessType/valuesOptions/ACCESS_TYPE_UNSPECIFIED/(gogoproto.enumvalue_customname)", + "value": "UnspecifiedAccess" + }, + { + "op": "add", + "path": "@/AccessType/values/ACCESS_TYPE_SUPER_FUN", + "value": 4 + }, + { + "op": "add", + "path": "@/AccessType/valuesOptions/ACCESS_TYPE_SUPER_FUN", + "value": { + "(gogoproto.enumvalue_customname)": "SuperFunAccessType" + } + } +] +``` + +### Modifying Message Fields + +Add or replace fields in a message: + +```json +[ + { + "op": "add", + "path": "@/Contract/fields/admin", + "value": { + "rule": "optional", + "type": "string", + "name": "admin", + "id": 3, + "options": { + "(gogoproto.moretags)": "yaml:\"admin\"" + } + } + } +] +``` + +## Best Practices + +1. **Use sparingly**: Only patch what's necessary while waiting for upstream changes +2. **Document patches**: Add comments explaining why each patch exists +3. **Review regularly**: Check if patches are still needed when upgrading dependencies +4. **Test thoroughly**: Ensure patched proto definitions work correctly +5. **Consider alternatives**: For extensive changes, consider maintaining a fork of the proto files + +## Limitations + +1. Not all proto structures can be modified (complex nested structures may be difficult) +2. Changes are applied during code generation and do not affect the original files +3. Patches may need to be updated if proto file structure changes significantly \ No newline at end of file diff --git a/docs/lcd-clients-classes.mdx b/docs/lcd-clients-classes.mdx new file mode 100644 index 0000000000..08eb5d6775 --- /dev/null +++ b/docs/lcd-clients-classes.mdx @@ -0,0 +1,389 @@ +# LCD Client Classes + +This document provides a reference for using and extending LCD Client Classes in Telescope-generated code. + +## Overview + +LCD Client Classes provide an object-oriented approach to interact with blockchain REST endpoints. Telescope generates these classes to enable modularity and extensibility while maintaining type safety. + +## Configuration + +To enable LCD Client Classes generation in Telescope: + +```typescript +import { TelescopeOptions } from "@cosmology/types"; + +const options: TelescopeOptions = { + lcdClientClasses: { + enabled: true + } +}; +``` + +## Configuration Parameters + +| Parameter | Type | Default | Description | +| --------- | ---- | ------- | ----------- | +| `lcdClientClasses.enabled` | boolean | `false` | Enables LCD Client Classes generation | +| `lcdClientClasses.bundle` | boolean | `true` | Whether to include classes in the bundle | +| `lcdClientClasses.methodName` | string | `"createLCDClientClasses"` | Factory method name | + +## Basic Usage + +### Creating LCD Client Classes + +```typescript +import { createLCDClientClasses } from "./codegen/client-classes"; + +// Create client classes +const clientClasses = createLCDClientClasses({ + restEndpoint: "https://rest.cosmos.network" +}); + +// Access modules using class instances +const bankModule = clientClasses.cosmos.bank.v1beta1; +const stakingModule = clientClasses.cosmos.staking.v1beta1; + +// Make queries +const balances = await bankModule.allBalances({ address: "cosmos1..." }); +const validators = await stakingModule.validators({}); +``` + +## Class Structure + +Each module is represented by a class with methods corresponding to the available queries: + +```typescript +class BankQueryClient { + constructor(protected readonly axios: AxiosInstance, protected readonly baseUrl: string) {} + + // Get all balances for an address + async allBalances(params: QueryAllBalancesRequest): Promise { + // Implementation details... + } + + // Get a specific token balance + async balance(params: QueryBalanceRequest): Promise { + // Implementation details... + } + + // Get total supply of all tokens + async totalSupply(params: QueryTotalSupplyRequest = {}): Promise { + // Implementation details... + } + + // ...other methods +} +``` + +## Extending Client Classes + +You can extend the generated classes to add custom functionality: + +```typescript +import { BankQueryClient } from "./codegen/cosmos/bank/v1beta1/query.lcd"; + +class ExtendedBankClient extends BankQueryClient { + // Add a convenience method for common operations + async getTokenBalance(address: string, denom: string): Promise { + const response = await this.balance({ + address, + denom + }); + return response.balance?.amount || "0"; + } + + // Add methods for formatted output + async getFormattedBalance(address: string, denom: string): Promise { + const amount = await this.getTokenBalance(address, denom); + + // Convert microunits to display units + const displayAmount = Number(amount) / 1_000_000; + + // Format the display amount and append symbol + const symbol = denom.startsWith('u') ? denom.substring(1).toUpperCase() : denom; + return `${displayAmount.toLocaleString()} ${symbol}`; + } +} +``` + +## Module Composition + +Combine multiple client classes to create a composite client: + +```typescript +import { BankQueryClient } from "./codegen/cosmos/bank/v1beta1/query.lcd"; +import { StakingQueryClient } from "./codegen/cosmos/staking/v1beta1/query.lcd"; +import { GovQueryClient } from "./codegen/cosmos/gov/v1beta1/query.lcd"; +import axios from "axios"; + +// Create a class that composes multiple module clients +class CompositeClient { + public readonly bank: BankQueryClient; + public readonly staking: StakingQueryClient; + public readonly gov: GovQueryClient; + + constructor(restEndpoint: string) { + const axiosInstance = axios.create({ baseURL: restEndpoint }); + + this.bank = new BankQueryClient(axiosInstance, restEndpoint); + this.staking = new StakingQueryClient(axiosInstance, restEndpoint); + this.gov = new GovQueryClient(axiosInstance, restEndpoint); + } + + // Add composite methods that use multiple modules + async getAccountOverview(address: string) { + const [balances, delegations, unbonding, rewards] = await Promise.all([ + this.bank.allBalances({ address }), + this.staking.delegatorDelegations({ delegatorAddr: address }), + this.staking.delegatorUnbondingDelegations({ delegatorAddr: address }), + this.gov.proposals({}) + ]); + + return { + availableBalances: balances.balances, + stakedBalance: delegations.delegationResponses, + unbondingBalance: unbonding.unbondingResponses, + activeProposals: rewards.proposals + }; + } +} +``` + +## Axios Configuration + +Customize the underlying Axios instance for better control: + +```typescript +import axios, { AxiosRequestConfig } from "axios"; +import { createLCDClientClasses } from "./codegen/client-classes"; + +// Custom Axios configuration +const axiosConfig: AxiosRequestConfig = { + timeout: 15000, + headers: { + "Accept": "application/json", + "Cache-Control": "no-cache" + }, + validateStatus: (status) => status < 500, // Only throw for server errors +}; + +// Create a custom Axios instance +const axiosInstance = axios.create(axiosConfig); + +// Add request interceptor for logging +axiosInstance.interceptors.request.use(config => { + console.log(`Making request to: ${config.url}`); + return config; +}); + +// Add response interceptor for error handling +axiosInstance.interceptors.response.use( + response => response, + error => { + console.error("API Error:", error.message); + return Promise.reject(error); + } +); + +// Create client classes with custom Axios instance +const clientClasses = createLCDClientClasses({ + restEndpoint: "https://rest.cosmos.network", + axios: axiosInstance +}); +``` + +## Implementing Caching + +Add caching to improve performance: + +```typescript +import { BankQueryClient } from "./codegen/cosmos/bank/v1beta1/query.lcd"; + +// Simple in-memory cache implementation +class CachingBankClient extends BankQueryClient { + private cache = new Map(); + private readonly TTL = 30000; // 30 seconds cache TTL + + private getCacheKey(method: string, params: any): string { + return `${method}:${JSON.stringify(params)}`; + } + + async allBalances(params: any): Promise { + const cacheKey = this.getCacheKey('allBalances', params); + + // Check cache + const cached = this.cache.get(cacheKey); + if (cached && (Date.now() - cached.timestamp) < this.TTL) { + return cached.data; + } + + // Not in cache, call parent method + const result = await super.allBalances(params); + + // Store in cache + this.cache.set(cacheKey, { + data: result, + timestamp: Date.now() + }); + + return result; + } + + // Implement similar caching for other methods +} +``` + +## Retry Logic + +Implement retry logic for resilient applications: + +```typescript +import { BankQueryClient } from "./codegen/cosmos/bank/v1beta1/query.lcd"; + +class RetryingBankClient extends BankQueryClient { + private maxRetries = 3; + private retryDelay = 1000; // 1 second + + private async withRetry(method: () => Promise): Promise { + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= this.maxRetries; attempt++) { + try { + return await method(); + } catch (error) { + lastError = error; + console.warn(`Request failed (attempt ${attempt}/${this.maxRetries}): ${error.message}`); + + if (attempt < this.maxRetries) { + // Wait before retrying (with exponential backoff) + const delay = this.retryDelay * Math.pow(2, attempt - 1); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + + throw lastError || new Error("Max retries exceeded"); + } + + async allBalances(params: any): Promise { + return this.withRetry(() => super.allBalances(params)); + } + + // Apply retry logic to other methods as needed +} +``` + +## Pagination Helpers + +Add pagination helpers to simplify working with paginated APIs: + +```typescript +import { StakingQueryClient } from "./codegen/cosmos/staking/v1beta1/query.lcd"; + +class PaginatedStakingClient extends StakingQueryClient { + // Get all validators with automatic pagination handling + async getAllValidators(params: any = {}): Promise { + let validators: any[] = []; + let nextKey: string | null = null; + + do { + // Prepare pagination parameters + const paginationParams = nextKey + ? { ...params, pagination: { ...params.pagination, key: nextKey } } + : params; + + // Call parent method + const response = await super.validators(paginationParams); + + // Add results to our collection + validators = [...validators, ...response.validators]; + + // Get next key for pagination + nextKey = response.pagination?.nextKey || null; + + } while (nextKey); + + return validators; + } +} +``` + +## Chain-specific Client Classes + +Different chains may have unique modules and classes: + +```typescript +import { GammQueryClient } from "./codegen/osmosis/gamm/v1beta1/query.lcd"; + +// Osmosis-specific client for liquidity pools +class OsmosisDexClient extends GammQueryClient { + // Get all pools with price information + async getPoolsWithPrices(): Promise { + const { pools } = await this.pools({}); + + return pools.map(pool => { + // Extract tokens from pool + const assets = pool.poolAssets || []; + + // Calculate prices based on pool ratios + const prices = this.calculatePrices(assets); + + return { + id: pool.id, + type: pool["@type"], + assets, + prices + }; + }); + } + + private calculatePrices(assets: any[]): Record { + // Price calculation logic + // This would calculate relative prices between assets in a pool + // ... + + return { /* calculated prices */ }; + } +} +``` + +## TypeScript Type Helpers + +Add TypeScript type helpers for better developer experience: + +```typescript +// Define helper types for better IntelliSense +export type CosmosAddress = string; +export type TokenAmount = string; +export type Denom = string; + +export interface Token { + denom: Denom; + amount: TokenAmount; +} + +// Extend the client with strongly typed methods +class TypedBankClient extends BankQueryClient { + async getBalance(address: CosmosAddress, denom: Denom): Promise { + const response = await this.balance({ + address, + denom + }); + + // Ensure we always return a valid token object + return response.balance || { denom, amount: "0" }; + } +} +``` + +## Best Practices + +1. **Extend don't modify**: Extend the generated classes rather than modifying them +2. **Share Axios instances**: Reuse the same Axios instance across client classes +3. **Add helper methods**: Implement domain-specific helper methods +4. **Implement caching**: Add caching for frequently accessed data +5. **Use type safety**: Leverage TypeScript's type system +6. **Handle failures gracefully**: Add retry logic and error handling +7. **Maintain modularity**: Create classes with a single responsibility +8. **Document extensions**: Add clear documentation for custom methods \ No newline at end of file diff --git a/docs/lcd-clients.mdx b/docs/lcd-clients.mdx new file mode 100644 index 0000000000..5e9f8dd540 --- /dev/null +++ b/docs/lcd-clients.mdx @@ -0,0 +1,272 @@ +# LCD Clients + +This document provides a reference for using LCD (Light Client Daemon) clients in Telescope-generated code to query blockchain data via REST endpoints. + +## Overview + +LCD clients provide a way to query blockchain state through REST API endpoints exposed by Cosmos SDK chains. Telescope can generate TypeScript clients that interact with these endpoints with full type safety. + +## Configuration Options + +To enable LCD client generation in Telescope, use the following options: + +```typescript +import { TelescopeOptions } from "@cosmology/types"; + +const options: TelescopeOptions = { + lcdClients: { + enabled: true, + // Additional optional configurations + scoped: [ + // Optional array of scoped client configurations + ] + } +}; +``` + +## Configuration Parameters + +| Parameter | Type | Default | Description | +| --------- | ---- | ------- | ----------- | +| `lcdClients.enabled` | boolean | `false` | Enables LCD client generation | +| `lcdClients.scoped` | array | `[]` | Configuration for scoped LCD clients | +| `lcdClients.scoped[].dir` | string | - | Directory to generate scoped client | +| `lcdClients.scoped[].filename` | string | - | Filename for the generated client | +| `lcdClients.scoped[].packages` | string[] | - | Array of packages to include | +| `lcdClients.scoped[].addToBundle` | boolean | `false` | Add to bundle export | +| `lcdClients.scoped[].methodName` | string | - | Method name for the client factory | + +## Basic Usage + +### Creating an LCD Client + +```typescript +import { createLCDClient } from "./codegen/client"; + +// Create the client +const client = await createLCDClient({ + restEndpoint: "https://rest.cosmos.network" +}); + +// Use the client to query blockchain data +const balanceResponse = await client.cosmos.bank.v1beta1.allBalances({ + address: "cosmos1..." +}); + +console.log("Balances:", balanceResponse.balances); +``` + +### Query Parameters + +Queries accept parameters based on the endpoint definition: + +```typescript +// Query with parameters +const validatorsResponse = await client.cosmos.staking.v1beta1.validators({ + status: "BOND_STATUS_BONDED", + pagination: { + key: "", + offset: "0", + limit: "100", + countTotal: true + } +}); + +console.log(`Found ${validatorsResponse.validators.length} active validators`); +``` + +## Module Organization + +LCD clients are organized by modules, versions, and query methods: + +```typescript +// Bank module +const bankModule = client.cosmos.bank.v1beta1; +const balances = await bankModule.allBalances({ address: "cosmos1..." }); + +// Staking module +const stakingModule = client.cosmos.staking.v1beta1; +const validators = await stakingModule.validators({}); + +// Governance module +const govModule = client.cosmos.gov.v1beta1; +const proposals = await govModule.proposals({}); +``` + +## Scoped LCD Clients + +For more focused applications, create scoped clients that include only specific modules: + +```typescript +// Configuration for generating scoped clients +const options: TelescopeOptions = { + lcdClients: { + enabled: true, + scoped: [ + { + dir: "osmosis", + filename: "osmosis-lcd-client.ts", + packages: [ + "cosmos.bank.v1beta1", + "osmosis.gamm.v1beta1", + "osmosis.lockup.v1beta1" + ], + addToBundle: true, + methodName: "createOsmosisLCDClient" + } + ] + } +}; + +// Using the generated scoped client +import { createOsmosisLCDClient } from "./codegen/osmosis/osmosis-lcd-client"; + +const osmosisClient = await createOsmosisLCDClient({ + restEndpoint: "https://rest.osmosis.zone" +}); + +// Now you can only access the modules specified in the configuration +const pools = await osmosisClient.osmosis.gamm.v1beta1.pools({}); +``` + +## Pagination + +Many queries support pagination for handling large datasets: + +```typescript +// First page +const firstPage = await client.cosmos.staking.v1beta1.validators({ + pagination: { + limit: "10" + } +}); + +// Next page, if there's more data +if (firstPage.pagination && firstPage.pagination.nextKey) { + const nextPage = await client.cosmos.staking.v1beta1.validators({ + pagination: { + key: firstPage.pagination.nextKey, + limit: "10" + } + }); +} +``` + +## Error Handling + +```typescript +try { + const response = await client.cosmos.bank.v1beta1.balance({ + address: "cosmos1...", + denom: "uatom" + }); + console.log("Balance:", response.balance); +} catch (error) { + if (error.response) { + // The request was made and the server responded with an error + console.error("Error status:", error.response.status); + console.error("Error data:", error.response.data); + } else if (error.request) { + // The request was made but no response was received + console.error("No response received:", error.request); + } else { + // Something happened in setting up the request + console.error("Request error:", error.message); + } +} +``` + +## Advanced: Creating Custom LCD Clients + +For custom chains or specific needs, you can create custom LCD clients: + +```typescript +import { LCDClient } from "@cosmology/lcd"; + +// Create a LCDClient with custom axios config +const client = new LCDClient({ + restEndpoint: "https://rest.cosmos.network", + axiosConfig: { + timeout: 15000, + headers: { + "Custom-Header": "Value" + } + } +}); + +// Register a custom endpoint +client.registerEndpoint({ + path: "/custom/endpoint", + method: "GET", + namespace: "custom", + operationId: "customQuery" +}); + +// Use the custom endpoint +const response = await client.custom.customQuery({ param: "value" }); +``` + +## Chain-specific Endpoints + +Different Cosmos chains may have unique modules and endpoints: + +### Osmosis DEX Queries + +```typescript +// Query liquidity pools on Osmosis +const poolsResponse = await osmosisClient.osmosis.gamm.v1beta1.pools({}); +console.log(`Found ${poolsResponse.pools.length} liquidity pools`); + +// Query specific pool +const poolResponse = await osmosisClient.osmosis.gamm.v1beta1.pool({ + poolId: "1" +}); +console.log("Pool details:", poolResponse.pool); +``` + +### CosmWasm Smart Contract Queries + +```typescript +// Query a CosmWasm smart contract +const contractResponse = await client.cosmwasm.wasm.v1.smartContractState({ + address: "juno1contract...", + queryData: btoa(JSON.stringify({ + get_config: {} + })) +}); + +// Parse the query result +const contractResult = JSON.parse( + new TextDecoder().decode(contractResponse.data) +); +console.log("Contract state:", contractResult); +``` + +## Response Types + +All LCD client responses are fully typed based on the Protobuf definitions: + +```typescript +// Response type for bank balance query +interface QueryAllBalancesResponse { + balances: Coin[]; + pagination?: PageResponse; +} + +// Response type for validator query +interface QueryValidatorsResponse { + validators: Validator[]; + pagination?: PageResponse; +} +``` + +## Best Practices + +1. **Use Typings**: Take advantage of TypeScript typings for autocomplete and type checking +2. **Handle Pagination**: For large datasets, implement proper pagination handling +3. **Cache Responses**: Consider caching responses for frequently queried data +4. **Error Handling**: Implement proper error handling for all queries +5. **Rate Limiting**: Be mindful of rate limits imposed by public REST endpoints +6. **Connection Management**: Close connections when they're no longer needed +7. **Timeout Configuration**: Set appropriate timeouts for network requests +8. **Prefer Specific Queries**: Use specific query methods when available instead of generic ones \ No newline at end of file diff --git a/docs/manually-registering-types.mdx b/docs/manually-registering-types.mdx index a789bde068..e6b678bd63 100644 --- a/docs/manually-registering-types.mdx +++ b/docs/manually-registering-types.mdx @@ -1,41 +1,263 @@ -## Manually registering types - -This example is with `osmosis` module in `osmojs`, but it is the same pattern for any module. - -NOTE: this is using `@cosmjs/stargate@0.28.4` - -```js -import { - AminoTypes, - SigningStargateClient -} from '@cosmjs/stargate'; -import { Registry } from '@cosmjs/proto-signing'; -import { defaultRegistryTypes } from '@cosmjs/stargate'; -import { OfflineSigner } from '@cosmjs/proto-signing' -import { osmosis } from 'osmojs'; - -export const getCustomSigningClient = async ({ rpcEndpoint, signer }: { rpcEndpoint: string, signer: OfflineSigner }) => { - // registry - const registry = new Registry(defaultRegistryTypes); - - // aminotypes - const aminoTypes = new AminoTypes({ - ...osmosis.gamm.v1beta1.AminoConverter, - ...osmosis.lockup.AminoConverter, - ...osmosis.superfluid.AminoConverter - }); - - // load the - osmosis.gamm.v1beta1.load(registry); - osmosis.lockup.load(registry); - osmosis.superfluid.load(registry); - - const client = await SigningStargateClient.connectWithSigner( - rpcEndpoint, - signer, - { registry, aminoTypes } - ); - - return client; +# Manually Registering Types + +This document provides a reference for manually registering types in Telescope-generated code. + +## Overview + +When working with Cosmos SDK applications, sometimes you need to extend Telescope's generated types with custom or third-party types. Manual type registration allows you to integrate these external types with Telescope's registry system. + +## Why Register Types Manually? + +Manual type registration is necessary when: + +1. Using types from third-party libraries not generated by Telescope +2. Working with custom types defined outside of Protocol Buffers +3. Integrating with chains or modules that have specialized type requirements +4. Creating hybrid applications with some manually defined types + +## Type Registry Basics + +Telescope generates registries for each message type: + +```typescript +// Example of a generated registry +export const registry = [ + ["/cosmos.bank.v1beta1.MsgSend", MsgSend], + ["/cosmos.staking.v1beta1.MsgDelegate", MsgDelegate], + // other message types... +]; +``` + +Each entry pairs a type URL with its corresponding TypeScript implementation. + +## Manual Registration Methods + +### Method 1: Adding to Default Registry + +```typescript +import { registry } from "./codegen/cosmos/bank/v1beta1/tx"; +import { GeneratedType } from "@cosmjs/proto-signing"; +import { MyCustomType } from "./my-custom-types"; + +// Add your custom type to the registry +const customRegistry: Array<[string, GeneratedType]> = [ + ...registry, + ["/my.custom.v1beta1.MyCustomType", MyCustomType] +]; + +// Use the extended registry when creating a client +const client = await SigningStargateClient.connectWithSigner( + rpcEndpoint, + signer, + { registry: customRegistry } +); +``` + +### Method 2: Using Registry Class + +```typescript +import { Registry } from "@cosmjs/proto-signing"; +import { defaultRegistryTypes } from "@cosmjs/stargate"; +import { MyCustomType } from "./my-custom-types"; + +// Create a new registry with default types +const registry = new Registry(defaultRegistryTypes); + +// Register your custom types +registry.register("/my.custom.v1beta1.MyCustomType", MyCustomType); + +// Use the registry +const client = await SigningStargateClient.connectWithSigner( + rpcEndpoint, + signer, + { registry } +); +``` + +### Method 3: Creating Registration Functions + +```typescript +import { Registry } from "@cosmjs/proto-signing"; +import { MsgCustom } from "./my-module/types"; + +// Create a registration function +export function registerMyModuleTypes(registry: Registry): void { + registry.register("/my.module.v1beta1.MsgCustom", MsgCustom); + // Register additional types here +} + +// Usage +const registry = new Registry(defaultRegistryTypes); +registerMyModuleTypes(registry); +``` + +## Implementation Requirements + +For a type to be registrable, it must implement the `GeneratedType` interface: + +```typescript +interface GeneratedType { + readonly typeUrl: string; + readonly encode: (message: any) => Uint8Array; + readonly decode: (binary: Uint8Array) => any; +} +``` + +When creating custom types, ensure they have appropriate encoding and decoding functions. + +## Custom Type Example + +```typescript +import { Writer, Reader } from "protobufjs/minimal"; + +// Define your custom type interface +export interface CustomMessage { + field1: string; + field2: number; +} + +// Implement the required methods +export const CustomMessage = { + typeUrl: "/my.custom.v1beta1.CustomMessage", + + encode(message: CustomMessage, writer: Writer = Writer.create()): Writer { + if (message.field1 !== "") { + writer.uint32(10).string(message.field1); + } + if (message.field2 !== 0) { + writer.uint32(16).int32(message.field2); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): CustomMessage { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { field1: "", field2: 0 } as CustomMessage; + + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.field1 = reader.string(); + break; + case 2: + message.field2 = reader.int32(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + + return message; + }, + + fromPartial(object: any): CustomMessage { + const message = { field1: "", field2: 0 } as CustomMessage; + if (object.field1 !== undefined && object.field1 !== null) { + message.field1 = object.field1; + } + if (object.field2 !== undefined && object.field2 !== null) { + message.field2 = object.field2; + } + return message; + } +}; +``` + +## Amino Type Registration + +For chains that use Amino (legacy encoding), you'll also need to register Amino converters: + +```typescript +import { AminoTypes } from "@cosmjs/stargate"; +import { AminoMsg } from "@cosmjs/amino"; + +// Define the Amino interface +interface AminoCustomMessage extends AminoMsg { + type: "my-module/CustomMessage"; + value: { + field1: string; + field2: string; // Note: numbers are strings in Amino + }; +} + +// Create Amino converters +const aminoConverters = { + "/my.custom.v1beta1.CustomMessage": { + aminoType: "my-module/CustomMessage", + toAmino: ({ field1, field2 }: CustomMessage): AminoCustomMessage["value"] => ({ + field1, + field2: field2.toString(), + }), + fromAmino: ({ field1, field2 }: AminoCustomMessage["value"]): CustomMessage => ({ + field1, + field2: parseInt(field2, 10), + }), + }, +}; + +// Register with AminoTypes +const aminoTypes = new AminoTypes(aminoConverters); +``` + +## Global Decoder Registry + +Telescope v1.0+ includes a global decoder registry system: + +```typescript +import { GlobalDecoderRegistry } from "./codegen/registry"; +import { CustomMessage } from "./my-custom-types"; + +// Register a custom type +GlobalDecoderRegistry.register(CustomMessage.typeUrl, CustomMessage); + +// Later, decode a message of any registered type +const decodedMessage = GlobalDecoderRegistry.decode(binaryData); +``` + +## Using Custom Types with Telescope Clients + +```typescript +import { getSigningCosmosClient } from "./codegen/client"; +import { CustomMessage } from "./my-custom-types"; +import { Registry } from "@cosmjs/proto-signing"; + +// Create a registry with custom types +const registry = new Registry(); +registry.register(CustomMessage.typeUrl, CustomMessage); + +// Create the signing client +const client = await getSigningCosmosClient({ + rpcEndpoint, + signer, + registry +}); + +// Now you can use custom message types +const customMsg = { + typeUrl: CustomMessage.typeUrl, + value: CustomMessage.fromPartial({ + field1: "value", + field2: 42 + }) }; + +// Use in transactions +const result = await client.signAndBroadcast( + signerAddress, + [customMsg], + fee +); ``` + +## Best Practices + +1. **Organize custom types** in dedicated modules for better maintainability +2. **Follow the same patterns** as Telescope-generated types for consistency +3. **Include proper TypeScript types** for better IDE support +4. **Implement all required methods** (encode, decode, fromPartial) +5. **Register both Proto and Amino types** if working with wallets that use Amino +6. **Use meaningful type URLs** that follow the pattern `/namespace.module.version.Type` +7. **Cache registries** where possible to avoid repeated registrations \ No newline at end of file diff --git a/docs/options.mdx b/docs/options.mdx index e47df67f57..df61ba47d8 100644 Binary files a/docs/options.mdx and b/docs/options.mdx differ diff --git a/docs/programatic-usage.mdx b/docs/programatic-usage.mdx deleted file mode 100644 index b001613c65..0000000000 --- a/docs/programatic-usage.mdx +++ /dev/null @@ -1,65 +0,0 @@ -## Programatic Usage - -First add telescope to your `devDependencies`: - -```sh -yarn add --dev @cosmology/telescope -``` - -Install helpers and cosmjs [dependencies listed here](./dependencies#dependencies) - -```js -import { join } from 'path'; -import telescope from '@cosmology/telescope'; -import { sync as rimraf } from 'rimraf'; - -const protoDirs = [join(__dirname, '/../proto')]; -const outPath = join(__dirname, '../src/codegen'); -rimraf(outPath); - -telescope({ - protoDirs, - outPath, - - // all options are totally optional ;) - options: { - aminoEncoding: { - enabled: true - }, - lcdClients: { - enabled: false - }, - rpcClients: { - enabled: false, - camelCase: true - }, - - // you can scope options to certain packages: - packages: { - nebula: { - prototypes: { - typingsFormat: { - useExact: false - } - } - }, - akash: { - stargateClients: { - enabled: true; - includeCosmosDefaultTypes: false; - }, - prototypes: { - typingsFormat: { - useExact: false - } - } - } - } - } -}).then(()=>{ - console.log('✨ all done!'); -}).catch(e=>{ - console.error(e); - process.exit(1); -}) -``` diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx new file mode 100644 index 0000000000..45a5b60524 --- /dev/null +++ b/docs/quickstart.mdx @@ -0,0 +1,68 @@ +# Quickstart + +This guide provides a quick reference for setting up and using Telescope to generate TypeScript packages for Cosmos SDK modules. +Here is the video for using create-interchain-app boilerplate: +https://youtu.be/iQf6p65fbdY + +## Installation + +Install create-interchain-app: + +```sh +npm install -g create-interchain-app +``` + +## Package Generation + +### Using create-interchain-app boilerplate + +Create a new package using the telescope boilerplate: + +```sh +cia --boilerplate telescope +``` + +Navigate to the generated project: + +```sh +cd ./your-project/packages/your-module +yarn install +``` + +## Working with Protos + +Download protocol buffer files: + +```sh +yarn download-protos +``` + +This will clone repositories into `./git-modules` and generate proto files in `./protos`. + +You can modify the configuration in ./scripts/download-protos.ts + +## Code Generation + +Generate TypeScript code from proto files: + +```sh +yarn codegen +``` + +You can modify the configuration in ./scripts/codegen.ts + +## Building + +Build the package for distribution: + +```sh +yarn build +``` + +## Publishing + +Use lerna for package management: + +```sh +lerna publish +``` \ No newline at end of file diff --git a/docs/rpc-client-classes.mdx b/docs/rpc-client-classes.mdx new file mode 100644 index 0000000000..5baa32980b --- /dev/null +++ b/docs/rpc-client-classes.mdx @@ -0,0 +1,470 @@ +# RPC Client Classes + +This document provides a reference for using and extending RPC Client Classes in Telescope-generated code. + +## Overview + +RPC Client Classes provide an object-oriented approach to interact with Cosmos SDK blockchains through RPC endpoints. Telescope generates these classes to enable extensibility and modularity while maintaining type safety. + +## Configuration + +To enable RPC Client Classes generation in Telescope: + +```typescript +import { TelescopeOptions } from "@cosmology/types"; + +const options: TelescopeOptions = { + rpcClientClasses: { + enabled: true + } +}; +``` + +## Configuration Parameters + +| Parameter | Type | Default | Description | +| --------- | ---- | ------- | ----------- | +| `rpcClientClasses.enabled` | boolean | `false` | Enables RPC Client Classes generation | +| `rpcClientClasses.camelCase` | boolean | `true` | Converts method names to camelCase | +| `rpcClientClasses.methodName` | string | `"createRPCQueryClientClasses"` | Factory method name | + +## Basic Usage + +### Creating RPC Client Classes + +```typescript +import { createRPCQueryClientClasses } from "./codegen/client-classes"; + +// Create client classes +const clientClasses = createRPCQueryClientClasses({ + rpcEndpoint: "https://rpc.cosmos.network" +}); + +// Access modules using class instances +const bankModule = clientClasses.cosmos.bank.v1beta1; +const stakingModule = clientClasses.cosmos.staking.v1beta1; + +// Make queries +const balances = await bankModule.allBalances({ address: "cosmos1..." }); +const validators = await stakingModule.validators({}); +``` + +## Class Structure + +Each module is represented by a class with query methods: + +```typescript +class BankQueryClient { + constructor( + protected readonly rpc: TendermintClient, + protected readonly queryClient: QueryClient + ) {} + + // Get all balances for an address + async allBalances(request: QueryAllBalancesRequest): Promise { + // Implementation details... + } + + // Get a specific token balance + async balance(request: QueryBalanceRequest): Promise { + // Implementation details... + } + + // Get total supply of all tokens + async totalSupply(request: QueryTotalSupplyRequest = {}): Promise { + // Implementation details... + } +} +``` + +## Client Types + +RPC Client Classes support different underlying implementations: + +```typescript +// Create client classes with specific client type +const tendermintClasses = createRPCQueryClientClasses({ + rpcEndpoint: "https://rpc.cosmos.network", + clientType: "tendermint" +}); + +const grpcWebClasses = createRPCQueryClientClasses({ + rpcEndpoint: "https://grpc-web.cosmos.network", + clientType: "grpc-web" +}); + +const grpcGatewayClasses = createRPCQueryClientClasses({ + rpcEndpoint: "https://rest.cosmos.network", + clientType: "grpc-gateway" +}); +``` + +## Extending Client Classes + +You can extend the generated classes to add custom functionality: + +```typescript +import { BankQueryClient } from "./codegen/cosmos/bank/v1beta1/query.rpc.Query"; + +class ExtendedBankClient extends BankQueryClient { + // Add a convenience method for common operations + async getTokenBalance(address: string, denom: string): Promise { + const response = await this.balance({ + address, + denom + }); + return response.balance?.amount || "0"; + } + + // Add methods for formatted output + async getFormattedBalance(address: string, denom: string): Promise { + const amount = await this.getTokenBalance(address, denom); + + // Convert microunits to display units + const displayAmount = Number(amount) / 1_000_000; + + // Format the display amount and append symbol + const symbol = denom.startsWith('u') ? denom.substring(1).toUpperCase() : denom; + return `${displayAmount.toLocaleString()} ${symbol}`; + } +} +``` + +## Module Composition + +Combine multiple client classes to create composite clients: + +```typescript +import { BankQueryClient } from "./codegen/cosmos/bank/v1beta1/query.rpc.Query"; +import { StakingQueryClient } from "./codegen/cosmos/staking/v1beta1/query.rpc.Query"; +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; +import { QueryClient } from "@cosmjs/stargate"; + +// Create a class that composes multiple module clients +class CompositeClient { + public readonly bank: BankQueryClient; + public readonly staking: StakingQueryClient; + + constructor(rpcEndpoint: string) { + // Create base Tendermint client + const tendermintClient = await Tendermint34Client.connect(rpcEndpoint); + const queryClient = new QueryClient(tendermintClient); + + // Create module clients + this.bank = new BankQueryClient(tendermintClient, queryClient); + this.staking = new StakingQueryClient(tendermintClient, queryClient); + } + + // Add composite methods that use multiple modules + async getAccountOverview(address: string) { + const [balances, delegations] = await Promise.all([ + this.bank.allBalances({ address }), + this.staking.delegatorDelegations({ delegatorAddr: address }) + ]); + + return { + availableBalances: balances.balances, + stakedBalance: delegations.delegationResponses + }; + } + + // Clean up resources + disconnect() { + this.bank.rpc.disconnect(); + } +} +``` + +## Working with Tendermint Subscriptions + +RPC Client Classes can be used with Tendermint WebSocket subscriptions: + +```typescript +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; +import { createRPCQueryClientClasses } from "./codegen/client-classes"; + +// Create raw Tendermint client for subscriptions +const tendermintClient = await Tendermint34Client.connect("wss://rpc.cosmos.network/websocket"); + +// Create RPC Client Classes using existing Tendermint client +const clientClasses = createRPCQueryClientClasses({ + rpcEndpoint: tendermintClient +}); + +// Set up a block subscription +const blockSubscription = tendermintClient.subscribeNewBlock().subscribe({ + next: async (block) => { + console.log(`New block: ${block.header.height}`); + + // Use clientClasses to query data related to this block + const validators = await clientClasses.cosmos.staking.v1beta1.validators({}); + console.log(`Current validators: ${validators.validators.length}`); + }, + error: (err) => { + console.error("Subscription error:", err); + } +}); + +// Later, clean up resources +blockSubscription.unsubscribe(); +tendermintClient.disconnect(); +``` + +## Implementing Retry Logic + +Add retry logic for resilient applications: + +```typescript +import { StakingQueryClient } from "./codegen/cosmos/staking/v1beta1/query.rpc.Query"; + +class RetryingStakingClient extends StakingQueryClient { + private maxRetries = 3; + private retryDelay = 1000; // 1 second + + private async withRetry(method: () => Promise): Promise { + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= this.maxRetries; attempt++) { + try { + return await method(); + } catch (error) { + lastError = error; + console.warn(`Request failed (attempt ${attempt}/${this.maxRetries}): ${error.message}`); + + if (attempt < this.maxRetries) { + // Wait before retrying (with exponential backoff) + const delay = this.retryDelay * Math.pow(2, attempt - 1); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + + throw lastError || new Error("Max retries exceeded"); + } + + async validators(request: any): Promise { + return this.withRetry(() => super.validators(request)); + } + + // Apply retry logic to other methods as needed +} +``` + +## Pagination Helpers + +Add pagination helpers to simplify working with paginated APIs: + +```typescript +import { StakingQueryClient } from "./codegen/cosmos/staking/v1beta1/query.rpc.Query"; + +class PaginatedStakingClient extends StakingQueryClient { + // Get all validators with automatic pagination handling + async getAllValidators(request: any = {}): Promise { + let validators: any[] = []; + let nextKey: string | null = null; + + do { + // Prepare pagination parameters + const paginationParams = nextKey + ? { ...request, pagination: { ...request.pagination, key: nextKey } } + : request; + + // Call parent method + const response = await super.validators(paginationParams); + + // Add results to our collection + validators = [...validators, ...response.validators]; + + // Get next key for pagination + nextKey = response.pagination?.nextKey || null; + + } while (nextKey); + + return validators; + } +} +``` + +## Chain-specific Client Classes + +Different chains may have unique modules: + +```typescript +import { GammQueryClient } from "./codegen/osmosis/gamm/v1beta1/query.rpc.Query"; + +// Osmosis-specific client for liquidity pools +class OsmosisDexClient extends GammQueryClient { + // Get pool with token prices + async getPoolWithPrices(poolId: string): Promise { + const { pool } = await this.pool({ poolId }); + + // Extract assets from pool + const assets = pool.poolAssets || []; + + // Calculate prices based on pool ratios + const prices = this.calculatePrices(assets); + + return { + id: pool.id, + type: pool["@type"], + assets, + prices + }; + } + + private calculatePrices(assets: any[]): Record { + // Price calculation logic + // ...implementation details... + + return { /* calculated prices */ }; + } +} +``` + +## TypeScript Type Helpers + +Add TypeScript type helpers for better developer experience: + +```typescript +// Define helper types for better IntelliSense +export type CosmosAddress = string; +export type TokenAmount = string; +export type Denom = string; + +export interface Token { + denom: Denom; + amount: TokenAmount; +} + +// Extend the client with strongly typed methods +class TypedBankClient extends BankQueryClient { + async getBalance(address: CosmosAddress, denom: Denom): Promise { + const response = await this.balance({ + address, + denom + }); + + // Ensure we always return a valid token object + return response.balance || { denom, amount: "0" }; + } +} +``` + +## Connection Management + +Proper connection management is important: + +```typescript +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; +import { QueryClient } from "@cosmjs/stargate"; +import { BankQueryClient } from "./codegen/cosmos/bank/v1beta1/query.rpc.Query"; + +class ManagedBankClient { + private tendermintClient: Tendermint34Client; + private queryClient: QueryClient; + private bankClient: BankQueryClient; + + static async connect(endpoint: string): Promise { + const instance = new ManagedBankClient(); + await instance.initialize(endpoint); + return instance; + } + + private async initialize(endpoint: string): Promise { + this.tendermintClient = await Tendermint34Client.connect(endpoint); + this.queryClient = new QueryClient(this.tendermintClient); + this.bankClient = new BankQueryClient(this.tendermintClient, this.queryClient); + } + + async getBalance(address: string, denom: string): Promise { + return this.bankClient.balance({ address, denom }); + } + + // Always call this when done with the client + disconnect(): void { + this.tendermintClient.disconnect(); + } +} + +// Usage +async function main() { + const client = await ManagedBankClient.connect("https://rpc.cosmos.network"); + try { + const balance = await client.getBalance("cosmos1...", "uatom"); + console.log(balance); + } finally { + client.disconnect(); // Always disconnect when done + } +} +``` + +## Request Batching + +For efficient network usage, batch related requests: + +```typescript +class BatchingClient { + private stakingClient: StakingQueryClient; + + constructor(stakingClient: StakingQueryClient) { + this.stakingClient = stakingClient; + } + + // Get validator details in batch + async getValidatorDetails(validatorAddresses: string[]): Promise { + // Create an array of promise-returning functions + const queries = validatorAddresses.map(address => + () => this.stakingClient.validator({ validatorAddr: address }) + ); + + // Execute with concurrency control + return this.batchExecute(queries, 5); + } + + // Generic batching helper with concurrency limit + private async batchExecute( + tasks: Array<() => Promise>, + concurrencyLimit: number + ): Promise { + const results: T[] = []; + const executing: Promise[] = []; + + for (const task of tasks) { + const p = task().then(result => { + results.push(result); + executing.splice(executing.indexOf(p), 1); + }); + + executing.push(p); + + if (executing.length >= concurrencyLimit) { + // Wait for one task to complete before adding more + await Promise.race(executing); + } + } + + // Wait for all tasks to complete + await Promise.all(executing); + + return results; + } +} +``` + +## Performance Considerations + +1. **Connection Reuse**: Create a single client and reuse it +2. **Pagination**: Use appropriate page sizes +3. **Batching**: Control concurrency for multiple requests +4. **Caching**: Consider caching for frequently accessed data +5. **Connection Pooling**: Use a connection pool for high-traffic applications + +## Best Practices + +1. **Extend don't modify**: Extend the generated classes rather than modifying them +2. **Resource Management**: Always disconnect clients when done +3. **Error Handling**: Implement proper error handling with retries +4. **Pagination**: Use key-based pagination for large datasets +5. **Type Safety**: Leverage TypeScript's type system +6. **Composition**: Compose multiple clients for complex functionality +7. **Client Lifecycle**: Manage connection lifecycle properly \ No newline at end of file diff --git a/docs/rpc-clients.mdx b/docs/rpc-clients.mdx new file mode 100644 index 0000000000..12f1652d4a --- /dev/null +++ b/docs/rpc-clients.mdx @@ -0,0 +1,386 @@ +# RPC Clients + +This document provides a reference for using RPC (Remote Procedure Call) clients in Telescope-generated code to interact with Cosmos SDK blockchains. + +## Overview + +Telescope supports generating TypeScript clients that can interact with Cosmos SDK chains through different RPC interfaces: + +1. **Tendermint RPC** - Direct access to Tendermint endpoints +2. **gRPC-web Client** - Browser-compatible gRPC client +3. **gRPC-gateway Client** - REST-based access to gRPC services + +## Configuration + +To enable RPC client generation in Telescope, use the following configuration: + +```typescript +import { TelescopeOptions } from "@cosmology/types"; + +const options: TelescopeOptions = { + rpcClients: { + enabled: true, + camelCase: true // Optional: convert method names to camelCase + } +}; +``` + +## Configuration Parameters + +| Parameter | Type | Default | Description | +| --------- | ---- | ------- | ----------- | +| `rpcClients.enabled` | boolean | `false` | Enables RPC client generation | +| `rpcClients.camelCase` | boolean | `true` | Converts RPC method names to camelCase | +| `rpcClients.bundle` | boolean | `true` | Include RPC clients in bundle | +| `rpcClients.methodName` | string | `"createRPCQueryClient"` | Factory method name | + +## Tendermint Client + +### Basic Usage + +```typescript +import { createRPCQueryClient } from "./codegen/rpc"; + +// Create a client +const client = await createRPCQueryClient({ + rpcEndpoint: "https://rpc.cosmos.network" +}); + +// Query blockchain data +const { validators } = await client.cosmos.staking.v1beta1.validators({}); +console.log(`Found ${validators.length} validators`); +``` + +### Tendermint-specific Endpoints + +```typescript +// Get node information +const nodeInfo = await client.cosmos.base.tendermint.v1beta1.getNodeInfo({}); +console.log("Chain ID:", nodeInfo.defaultNodeInfo.network); + +// Get latest block +const latestBlock = await client.cosmos.base.tendermint.v1beta1.getLatestBlock({}); +console.log("Latest block height:", latestBlock.block.header.height); + +// Get syncing status +const syncingStatus = await client.cosmos.base.tendermint.v1beta1.getSyncing({}); +console.log("Node is syncing:", syncingStatus.syncing); +``` + +## gRPC-web Client + +For browser applications, use the gRPC-web client: + +```typescript +import { createRPCQueryClient } from "./codegen/rpc"; + +// Create a gRPC-web client +const client = await createRPCQueryClient({ + rpcEndpoint: "https://grpc-web.cosmos.network" +}); + +// Use the same interface as Tendermint client +const { balance } = await client.cosmos.bank.v1beta1.balance({ + address: "cosmos1...", + denom: "uatom" +}); +``` + +## gRPC-gateway Client + +For REST-based access to gRPC services: + +```typescript +import { createRPCQueryClient } from "./codegen/rpc"; + +// Create a client using gRPC-gateway endpoint +const client = await createRPCQueryClient({ + rpcEndpoint: "https://rest.cosmos.network" +}); + +// Query as normal +const { supply } = await client.cosmos.bank.v1beta1.totalSupply({}); +``` + +## Client Types + +Specify the client type explicitly: + +```typescript +import { createRPCQueryClient } from "./codegen/rpc"; + +// Choose the client implementation +const client = await createRPCQueryClient({ + rpcEndpoint: "https://rpc.cosmos.network", + clientType: "tendermint" // or "grpc-web" or "grpc-gateway" +}); +``` + +## Module Organization + +RPC clients are organized by modules, versions, and methods: + +```typescript +// Bank module +const bank = client.cosmos.bank.v1beta1; +const balances = await bank.allBalances({ address: "cosmos1..." }); + +// Staking module +const staking = client.cosmos.staking.v1beta1; +const validators = await staking.validators({}); + +// Governance module +const gov = client.cosmos.gov.v1beta1; +const proposals = await gov.proposals({}); +``` + +## Pagination + +Many RPC methods support pagination for handling large result sets: + +```typescript +// First page of validators +const { validators, pagination } = await client.cosmos.staking.v1beta1.validators({ + pagination: { + limit: "10", + offset: "0", + countTotal: true + } +}); + +console.log(`Page 1: ${validators.length} validators`); +console.log(`Total count: ${pagination.total}`); + +// Next page +const nextPage = await client.cosmos.staking.v1beta1.validators({ + pagination: { + limit: "10", + offset: "10", + countTotal: false + } +}); + +console.log(`Page 2: ${nextPage.validators.length} validators`); +``` + +## Key-based Pagination + +For larger datasets, use key-based pagination: + +```typescript +let nextKey = null; +let allValidators = []; + +do { + // Query with nextKey if available + const paginationParams = nextKey + ? { key: nextKey, limit: "100" } + : { limit: "100" }; + + const response = await client.cosmos.staking.v1beta1.validators({ + pagination: paginationParams + }); + + // Add results to collection + allValidators = [...allValidators, ...response.validators]; + + // Get the next pagination key + nextKey = response.pagination?.nextKey; + +} while (nextKey && nextKey.length > 0); + +console.log(`Retrieved ${allValidators.length} validators in total`); +``` + +## Error Handling + +```typescript +try { + const { balance } = await client.cosmos.bank.v1beta1.balance({ + address: "cosmos1...", + denom: "uatom" + }); + console.log("Balance:", balance); +} catch (error) { + if (error.code === 3) { // NOT_FOUND in gRPC + console.error("Address not found"); + } else if (error.code === 4) { // DEADLINE_EXCEEDED + console.error("Request timed out"); + } else if (error.code === 13) { // INTERNAL + console.error("Internal server error"); + } else { + console.error("Error:", error.message); + } +} +``` + +## Custom Endpoints + +For custom or extended RPC endpoints: + +```typescript +import { QueryClient } from "@cosmjs/stargate"; +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; +import { setupCustomExtension } from "./my-extension"; + +// Create a Tendermint client +const tendermintClient = await Tendermint34Client.connect("https://rpc.cosmos.network"); + +// Create a QueryClient with custom extension +const queryClient = QueryClient.withExtensions( + tendermintClient, + setupCustomExtension +); + +// Use the custom extension +const customResult = await queryClient.custom.myCustomQuery(params); +``` + +## RPC Client Comparison + +| Feature | Tendermint RPC | gRPC-web | gRPC-gateway | +| ------- | ------------- | -------- | ------------ | +| Browser Support | Limited | Yes | Yes | +| Efficiency | High | Medium | Low | +| Streaming | Yes | Limited | No | +| Compatibility | Node.js | All | All | +| Communication | Direct | HTTP/1.1 + Protobuf | HTTP/REST | + +## Tendermint RPC Specific Methods + +```typescript +// Create a raw Tendermint client +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; + +const tmClient = await Tendermint34Client.connect("https://rpc.cosmos.network"); + +// Get blockchain information +const abciInfo = await tmClient.abciInfo(); +console.log("ABCI application version:", abciInfo.applicationVersion); + +// Get a block by height +const block = await tmClient.block(123456); +console.log("Block time:", block.block.header.time); + +// Get validator set at a height +const validators = await tmClient.validatorsAll(123456); +console.log("Validator count:", validators.length); + +// Subscribe to new blocks +const subscription = tmClient.subscribeNewBlock().subscribe({ + next: (event) => { + console.log("New block:", event.header.height); + }, + error: (error) => { + console.error("Subscription error:", error); + }, + complete: () => { + console.log("Subscription completed"); + } +}); + +// Later, unsubscribe +subscription.unsubscribe(); +``` + +## WebSocket Subscriptions + +```typescript +// Subscribe to transaction events +const subscription = tmClient.subscribeTx().subscribe({ + next: (event) => { + console.log("Transaction hash:", event.hash); + console.log("Transaction result:", event.result); + }, + error: (error) => { + console.error("Subscription error:", error); + } +}); + +// Subscribe to specific events +const subscription = tmClient.subscribeNewBlockHeader().subscribe({ + next: (header) => { + console.log("New block height:", header.height); + console.log("Block time:", header.time); + console.log("Proposer:", header.proposerAddress); + } +}); +``` + +## Request Batching + +For efficient network usage, batch related requests: + +```typescript +import { createRPCQueryClient } from "./codegen/rpc"; + +async function getBatchedData(addresses) { + const client = await createRPCQueryClient({ + rpcEndpoint: "https://rpc.cosmos.network" + }); + + // Execute multiple queries in parallel + const balancePromises = addresses.map(address => + client.cosmos.bank.v1beta1.allBalances({ address }) + ); + + // Wait for all queries to complete + const balanceResults = await Promise.all(balancePromises); + + // Process results + return addresses.map((address, index) => ({ + address, + balances: balanceResults[index].balances + })); +} + +// Example usage +const addresses = [ + "cosmos1address1...", + "cosmos1address2...", + "cosmos1address3..." +]; + +const results = await getBatchedData(addresses); +console.log(results); +``` + +## Connection Management + +```typescript +import { createRPCQueryClient } from "./codegen/rpc"; +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; + +// Create a Tendermint client directly +const tmClient = await Tendermint34Client.connect("https://rpc.cosmos.network"); + +// Create RPC client using existing Tendermint client +const client = await createRPCQueryClient({ + rpcEndpoint: tmClient +}); + +// Use the client +const chainId = await client.cosmos.base.tendermint.v1beta1.getNodeInfo({}); + +// When done, disconnect +tmClient.disconnect(); +``` + +## Performance Considerations + +1. **Connection Reuse**: Create a single client and reuse it +2. **Pagination**: Use appropriate page sizes +3. **Batching**: Batch related requests with Promise.all +4. **Disconnection**: Always disconnect when done +5. **Error Handling**: Implement proper error handling and retries +6. **Timeouts**: Set appropriate timeouts for RPC calls + +## Best Practices + +1. **Client Lifecycle**: Manage client connections properly +2. **Error Handling**: Implement proper error handling with retries +3. **Batching**: Batch related requests for efficiency +4. **Pagination**: Use pagination for large datasets +5. **Disconnection**: Always disconnect clients when done +6. **Type Safety**: Leverage TypeScript typing for better developer experience +7. **Authentication**: Include authentication if required by the endpoint \ No newline at end of file diff --git a/docs/sponsors.mdx b/docs/sponsors.mdx new file mode 100644 index 0000000000..9a829e66b5 --- /dev/null +++ b/docs/sponsors.mdx @@ -0,0 +1,136 @@ +# Sponsors + +This document provides information about the organizations and individuals who sponsor and support the Telescope project. + +## Current Sponsors + +### Hyperweb + +Hyperweb Logo + +[Hyperweb](https://hyperweb.ai) is the primary sponsor and maintainer of the Telescope project. Hyperweb is dedicated to building developer tools and infrastructure for web3 and the Cosmos ecosystem. + +### Cosmology + +Cosmology Logo + +[Cosmology](https://cosmology.tech) provides substantial support for Telescope development and maintenance. Cosmology specializes in developer tooling and infrastructure for the Cosmos ecosystem. + +### Interchain Foundation + +Interchain Foundation Logo + +The [Interchain Foundation](https://interchain.io) has provided grants to support Telescope development, particularly features that benefit the broader Cosmos ecosystem. + +## Corporate Sponsors + +The following organizations provide financial or resource support for Telescope: + +| Sponsor | Contribution | +| ------- | ------------ | +| Osmosis Labs | Financial support, testing, integration | +| Akash Network | Infrastructure resources | +| Juno Network | Financial support, feedback | +| Keplr Wallet | Integration testing, feedback | +| Cosmos Hub | Financial support through grants | + +## Individual Sponsors + +Many individual developers and community members contribute to Telescope through GitHub Sponsors, grants, or direct contributions. + +## Open Collective + +Telescope is supported through [Open Collective](https://opencollective.com/telescope), which provides a transparent way to support the project financially. + +## Becoming a Sponsor + +If you or your organization uses Telescope and would like to support its continued development, there are several ways to become a sponsor: + +### GitHub Sponsors + +You can sponsor the project through GitHub Sponsors, which allows for recurring monthly donations: + +[Sponsor Telescope on GitHub](https://github.com/sponsors/hyperweb-io) + +### Open Collective + +For organizations that prefer invoicing or require more formal financial agreements: + +[Sponsor Telescope on Open Collective](https://opencollective.com/telescope) + +### Custom Sponsorship Agreements + +For larger sponsorships or custom arrangements, please contact the maintainers directly at [sponsors@hyperweb.ai](mailto:sponsors@hyperweb.ai). + +## Sponsorship Tiers + +| Tier | Monthly Contribution | Benefits | +| ---- | -------------------- | -------- | +| Community Supporter | $5-$49 | Name in sponsors list, Discord role | +| Bronze Sponsor | $50-$199 | Logo in README, priority issue responses | +| Silver Sponsor | $200-$499 | Medium logo in README, consulting hours | +| Gold Sponsor | $500-$999 | Large logo in README and docs, integration support | +| Platinum Sponsor | $1000+ | Premium placement, dedicated support channel, prioritized feature development | + +## Sponsor Recognition + +### README + +All sponsors at Bronze tier and above are recognized in the main README.md file of the project. + +### Documentation + +Gold and Platinum sponsors are recognized throughout the documentation. + +### Website + +All sponsors are listed on the Telescope website with logo placement according to sponsorship tier. + +## How Sponsorship Funds Are Used + +Sponsorship funds are used to: + +1. Maintain and improve the core Telescope library +2. Develop new features +3. Fix bugs and address security issues +4. Improve documentation +5. Provide support to users +6. Cover infrastructure costs +7. Compensate maintainers and contributors + +## Transparency + +Telescope maintains transparency in how sponsorship funds are used: + +- Financial reports are published quarterly on Open Collective +- Major expenditures are announced to sponsors +- Development priorities influenced by sponsors are clearly labeled + +## Sponsor-Driven Development + +While sponsorship does influence development priorities, Telescope maintains a commitment to open-source principles and the broader community's needs. + +Features requested by sponsors are prioritized based on: + +1. Alignment with project roadmap +2. Benefit to the broader community +3. Technical feasibility +4. Sponsorship level + +## Current Funding Goals + +| Goal | Target | Progress | Description | +| ---- | ------ | -------- | ----------- | +| Full-time Maintainer | $8,000/mo | 65% | Fund a full-time maintainer position | +| Documentation Overhaul | $5,000 | 90% | Complete documentation rewrite and examples | +| React Integration | $3,000 | 100% | React hooks and components (Completed) | +| Vue Integration | $3,000 | 40% | Vue composables and components | + +## Contact + +For questions about sponsorship or to discuss custom sponsorship arrangements, please contact: + +- Email: [sponsors@hyperweb.ai](mailto:sponsors@hyperweb.ai) +- Discord: Join the [Telescope Discord](https://discord.gg/telescope) and message the maintainers + +Thank you to all our sponsors for making Telescope possible! \ No newline at end of file diff --git a/docs/stack.mdx b/docs/stack.mdx new file mode 100644 index 0000000000..b151290707 --- /dev/null +++ b/docs/stack.mdx @@ -0,0 +1,228 @@ +# Technology Stack + +This document provides a reference for the technology stack used by Telescope and Telescope-generated code. + +## Core Technologies + +### TypeScript + +Telescope is built with TypeScript and generates TypeScript code. TypeScript provides static typing that helps catch errors early in the development process and improves developer productivity through better tooling. + +**Version**: 4.7+ + +**Resources**: +- [TypeScript Documentation](https://www.typescriptlang.org/docs/) +- [TypeScript GitHub Repository](https://github.com/microsoft/TypeScript) + +### Protocol Buffers + +Telescope uses Protocol Buffers (Protobuf), a language-neutral, platform-neutral, extensible mechanism for serializing structured data. Cosmos SDK chains use Protobuf for defining messages, services, and types. + +**Version**: proto3 + +**Resources**: +- [Protocol Buffers Documentation](https://developers.google.com/protocol-buffers) +- [proto3 Language Guide](https://developers.google.com/protocol-buffers/docs/proto3) + +### CosmJS + +Telescope-generated code relies on CosmJS, a library for building JavaScript applications that interact with Cosmos SDK blockchains. + +**Version**: 0.29+ (recommended 0.31+) + +**Key Packages**: +- `@cosmjs/stargate` - High-level client for Cosmos SDK chains +- `@cosmjs/proto-signing` - Protobuf-based transaction signing +- `@cosmjs/tendermint-rpc` - Interface to Tendermint RPC +- `@cosmjs/amino` - Legacy Amino encoding support + +**Resources**: +- [CosmJS Documentation](https://cosmos.github.io/cosmjs/) +- [CosmJS GitHub Repository](https://github.com/cosmos/cosmjs) + +## Build System + +### Node.js + +Telescope runs on Node.js, a JavaScript runtime built on Chrome's V8 JavaScript engine. + +**Version**: 14+ (recommended 16+) + +**Resources**: +- [Node.js Documentation](https://nodejs.org/en/docs/) + +### Yarn + +Telescope uses Yarn for package management. + +**Version**: 1.x + +**Resources**: +- [Yarn Documentation](https://classic.yarnpkg.com/en/docs) + +### Lerna + +Telescope is organized as a monorepo using Lerna for managing multiple packages. + +**Version**: 4.x + +**Resources**: +- [Lerna Documentation](https://lerna.js.org/) + +## Code Generation + +### AST Manipulation + +Telescope uses Abstract Syntax Tree (AST) manipulation to generate TypeScript code from Protobuf definitions. + +**Key Technologies**: +- TypeScript Compiler API +- Custom AST processing + +### Templates + +Telescope uses code templates for generating consistent patterns across different services and message types. + +## Optional Integrations + +### React Query + +Telescope can generate React Query hooks for interacting with Cosmos SDK chains. + +**Version**: 4.x + +**Resources**: +- [TanStack Query Documentation](https://tanstack.com/query/v4/docs/overview) + +### Vue Query + +Telescope can generate Vue Query composables for Vue.js applications. + +**Version**: 4.x + +**Resources**: +- [Vue Query Documentation](https://tanstack.com/query/v4/docs/adapters/vue-query) + +### Recoil + +Telescope supports integration with Recoil for React state management. + +**Version**: 0.7+ + +**Resources**: +- [Recoil Documentation](https://recoiljs.org/docs/introduction/installation) + +### CosmWasm + +Telescope integrates with CosmWasm for generating TypeScript clients for CosmWasm smart contracts. + +**Technology**: +- [@cosmwasm/ts-codegen](https://github.com/CosmWasm/ts-codegen) + +**Resources**: +- [CosmWasm Documentation](https://docs.cosmwasm.com/) + +## Development Tools + +### Jest + +Telescope uses Jest for testing. + +**Version**: 27+ + +**Resources**: +- [Jest Documentation](https://jestjs.io/docs/getting-started) + +### ESLint + +Telescope uses ESLint for code linting. + +**Version**: 8+ + +**Resources**: +- [ESLint Documentation](https://eslint.org/docs/user-guide/getting-started) + +### Prettier + +Telescope uses Prettier for code formatting. + +**Version**: 2+ + +**Resources**: +- [Prettier Documentation](https://prettier.io/docs/en/index.html) + +## Runtime Dependencies for Generated Code + +### Long.js + +Used for handling 64-bit integers in JavaScript. + +**Resources**: +- [Long.js GitHub Repository](https://github.com/dcodeIO/long.js) + +### buffer + +Provides Buffer implementation for browser environments. + +**Resources**: +- [buffer npm package](https://www.npmjs.com/package/buffer) + +## Browser Compatibility + +Telescope-generated code is compatible with modern browsers. For older browsers, polyfills may be required: + +**Required Polyfills**: +- `Promise` +- `fetch` +- `TextEncoder`/`TextDecoder` +- `Buffer` + +**Recommended Polyfill Solution**: +- `core-js` for standard JavaScript features +- Custom polyfills for Node.js built-ins in browser environments + +## Recommended Development Environment + +- **IDE**: Visual Studio Code with TypeScript support +- **Node Version Manager**: nvm or volta +- **Package Manager**: Yarn 1.x +- **Git Hooks**: husky with lint-staged + +## Version Compatibility Matrix + +| Telescope Version | TypeScript | Node.js | CosmJS | Protobuf | +| ----------------- | ---------- | ------- | ------ | -------- | +| 1.0.x | 4.7+ | 14+ | 0.29+ | proto3 | +| 0.12.x | 4.5+ | 14+ | 0.28+ | proto3 | +| 0.11.x | 4.4+ | 12+ | 0.27+ | proto3 | + +## Deployment Environments + +Telescope-generated code can be deployed in various environments: + +### Browser + +- Webpack, Rollup, or Vite for bundling +- Polyfills for Node.js built-ins +- CORS considerations for RPC endpoints + +### Node.js + +- Direct usage without special configuration +- Better performance for cryptographic operations + +### React Native + +- Requires appropriate polyfills +- May need native modules for cryptography + +## Resource Requirements + +Typical resource requirements for running Telescope: + +| Resource | Minimum | Recommended | +| ----------------- | ------- | ----------- | +| CPU | 2 cores | 4+ cores | +| Memory | 2 GB | 4+ GB | +| Disk Space | 1 GB | 5+ GB | +| Node.js Version | 14.x | 16.x+ | \ No newline at end of file diff --git a/docs/stargate-clients.mdx b/docs/stargate-clients.mdx new file mode 100644 index 0000000000..29b41e182e --- /dev/null +++ b/docs/stargate-clients.mdx @@ -0,0 +1,352 @@ +# Stargate Clients + +This document provides a reference for using Stargate clients with Telescope-generated types to interact with Cosmos SDK blockchains. + +## Overview + +Stargate clients facilitate communication between your application and Cosmos SDK blockchains. Telescope generates the necessary type definitions and registries to use these clients with type safety. + +## Client Types + +| Client Type | Purpose | Package | +| ----------- | ------- | ------- | +| `QueryClient` | Read-only queries of blockchain state | `@cosmjs/stargate` | +| `SigningStargateClient` | Send signed transactions | `@cosmjs/stargate` | +| `StargateClient` | Basic read-only operations | `@cosmjs/stargate` | + +## Registry Configuration + +The registry maps Protobuf type URLs to their corresponding TypeScript types: + +```typescript +import { Registry } from "@cosmjs/proto-signing"; +import { defaultRegistryTypes } from "@cosmjs/stargate"; +import { registry as telescopeRegistry } from "./registry"; + +// Create a registry with default types plus Telescope-generated types +function createRegistry(): Registry { + const registry = new Registry(defaultRegistryTypes); + + // Register Telescope-generated types + telescopeRegistry.forEach(([typeUrl, type]) => { + registry.register(typeUrl, type); + }); + + return registry; +} +``` + +## Creating a StargateClient + +```typescript +import { StargateClient } from "@cosmjs/stargate"; + +// Connect to a Cosmos SDK chain +const client = await StargateClient.connect("https://rpc.cosmos.network"); + +// Basic queries +const height = await client.getHeight(); +const balance = await client.getAllBalances("cosmos1..."); +const account = await client.getAccount("cosmos1..."); +``` + +## Creating a QueryClient + +```typescript +import { QueryClient, setupStargateClient } from "@cosmjs/stargate"; +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; + +// Create a Tendermint client +const tmClient = await Tendermint34Client.connect("https://rpc.cosmos.network"); + +// Create a query client +const queryClient = QueryClient.withExtensions( + tmClient, + setupStargateClient +); + +// Query chain ID and height +const chainId = await queryClient.stargateClient.getChainId(); +const height = await queryClient.stargateClient.getHeight(); +``` + +## Querying with Module Extensions + +```typescript +import { setupBankExtension, BankExtension } from "@cosmjs/stargate"; + +// Query client with bank module extension +const queryClient = QueryClient.withExtensions( + tmClient, + setupBankExtension +); + +// Query balances using the extension +const balance = await queryClient.bank.allBalances("cosmos1..."); +``` + +## Creating a SigningStargateClient + +```typescript +import { SigningStargateClient } from "@cosmjs/stargate"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { registry } from "./registry"; +import { GasPrice } from "@cosmjs/stargate"; + +// Create a wallet +const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + "your mnemonic here", + { prefix: "cosmos" } +); + +// Create a signing client +const client = await SigningStargateClient.connectWithSigner( + "https://rpc.cosmos.network", + wallet, + { + registry, + gasPrice: GasPrice.fromString("0.025uatom") + } +); + +// Get the first account from the wallet +const [account] = await wallet.getAccounts(); + +// Send a transaction +const result = await client.sendTokens( + account.address, + "cosmos1recipient...", + [{ denom: "uatom", amount: "1000000" }], + { + amount: [{ denom: "uatom", amount: "5000" }], + gas: "200000" + } +); +``` + +## Using Telescope-generated Types with SigningStargateClient + +```typescript +import { SigningStargateClient } from "@cosmjs/stargate"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { registry } from "./registry"; +import { MsgSend } from "./cosmos/bank/v1beta1/tx"; + +// Create client and wallet +const wallet = await DirectSecp256k1HdWallet.fromMnemonic("..."); +const client = await SigningStargateClient.connectWithSigner( + "https://rpc.cosmos.network", + wallet, + { registry } +); +const [account] = await wallet.getAccounts(); + +// Create a MsgSend using Telescope-generated types +const sendMsg = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: MsgSend.fromPartial({ + fromAddress: account.address, + toAddress: "cosmos1recipient...", + amount: [{ denom: "uatom", amount: "1000000" }] + }) +}; + +// Send the transaction +const result = await client.signAndBroadcast( + account.address, + [sendMsg], + { + amount: [{ denom: "uatom", amount: "5000" }], + gas: "200000" + } +); +``` + +## Amino Converters Configuration + +```typescript +import { SigningStargateClient, AminoTypes } from "@cosmjs/stargate"; +import { aminoConverters } from "./amino/converters"; + +// Create a custom AminoTypes instance with Telescope-generated converters +const customAminoTypes = new AminoTypes(aminoConverters); + +// Create a signing client with custom Amino types +const client = await SigningStargateClient.connectWithSigner( + "https://rpc.cosmos.network", + wallet, + { + registry, + aminoTypes: customAminoTypes + } +); +``` + +## Telescope-specific Stargate Client + +```typescript +import { createStargateClient } from "./stargate"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; + +// Create Telescope's enhanced Stargate client +const client = await createStargateClient({ + rpcEndpoint: "https://rpc.cosmos.network", + signer: wallet +}); + +// Use convenience methods for sending messages +const result = await client.sendMsgs( + "cosmos1sender...", + [sendMsg, delegateMsg], + fee +); +``` + +## Client Configuration Options + +| Option | Description | Default | +| ------ | ----------- | ------- | +| `registry` | Maps type URLs to their TypeScript types | Default registry | +| `aminoTypes` | Custom Amino converters for legacy compatibility | Default converters | +| `gasPrice` | Price per unit of gas | Chain-dependent | +| `broadcastTimeoutMs` | Max wait time for transaction broadcasting | 60000 | +| `broadcastPollIntervalMs` | Polling interval for transaction confirmation | 3000 | +| `prefix` | Bech32 address prefix | "cosmos" | + +## Error Handling + +```typescript +try { + const result = await client.signAndBroadcast( + account.address, + messages, + fee + ); + + if (result.code === 0) { + console.log("Transaction successful:", result.transactionHash); + } else { + console.error("Transaction failed with code:", result.code); + console.error("Error message:", result.rawLog); + } +} catch (error) { + if (error.message.includes("Account does not exist")) { + console.error("Account not found on chain. Make sure it's funded."); + } else if (error.message.includes("insufficient fees")) { + console.error("Insufficient fees provided."); + } else if (error.message.includes("out of gas")) { + console.error("Transaction ran out of gas."); + } else { + console.error("Transaction error:", error); + } +} +``` + +## Reading Transaction Results + +```typescript +// Send transaction +const result = await client.signAndBroadcast( + account.address, + messages, + fee +); + +// Access transaction data +console.log("Transaction hash:", result.transactionHash); +console.log("Block height:", result.height); +console.log("Gas used:", result.gasUsed); +console.log("Gas wanted:", result.gasWanted); + +// Access events and attributes +result.events.forEach(event => { + console.log("Event type:", event.type); + event.attributes.forEach(attr => { + console.log(` ${attr.key}: ${attr.value}`); + }); +}); +``` + +## Custom Query Extensions + +```typescript +import { QueryClient } from "@cosmjs/stargate"; +import { createProtobufRpcClient, QueryClient as TMClient } from "@cosmjs/stargate"; +import { QueryClientImpl } from "./cosmos/bank/v1beta1/query"; + +// Create a query extension for a specific module +function setupCustomBankExtension(base: TMClient) { + const rpc = createProtobufRpcClient(base); + const queryService = new QueryClientImpl(rpc); + + return { + bank: { + balance: async (address: string, denom: string) => { + const { balance } = await queryService.Balance({ + address, + denom + }); + return balance; + }, + allBalances: async (address: string) => { + const { balances } = await queryService.AllBalances({ address }); + return balances; + } + } + }; +} + +// Use the custom extension +const queryClient = QueryClient.withExtensions( + tmClient, + setupCustomBankExtension +); + +const balance = await queryClient.bank.balance("cosmos1...", "uatom"); +``` + +## Chain-specific Configurations + +```typescript +import { GasPrice } from "@cosmjs/stargate"; + +// Chain-specific configurations +const chainConfigs = { + cosmos: { + rpcEndpoint: "https://rpc.cosmos.network", + prefix: "cosmos", + gasPrice: GasPrice.fromString("0.025uatom"), + feeDenom: "uatom" + }, + osmosis: { + rpcEndpoint: "https://rpc.osmosis.zone", + prefix: "osmo", + gasPrice: GasPrice.fromString("0.025uosmo"), + feeDenom: "uosmo" + } +}; + +// Create a client for a specific chain +const chainId = "osmosis-1"; +const config = chainConfigs.osmosis; + +const client = await SigningStargateClient.connectWithSigner( + config.rpcEndpoint, + wallet, + { + registry, + prefix: config.prefix, + gasPrice: config.gasPrice + } +); +``` + +## Best Practices + +1. Always use a specific registry with Telescope-generated types +2. Handle network connection errors gracefully +3. Implement proper transaction result parsing +4. Set reasonable timeout values for network operations +5. Use appropriate gas prices for each chain +6. Close Tendermint clients when done to free resources +7. Implement proper error handling for chain-specific errors \ No newline at end of file diff --git a/docs/troubleshooting.mdx b/docs/troubleshooting.mdx new file mode 100644 index 0000000000..40af127808 --- /dev/null +++ b/docs/troubleshooting.mdx @@ -0,0 +1,247 @@ +# Troubleshooting + +This document provides solutions for common issues encountered when using Telescope and Telescope-generated code. + +## Common Issues + +### Webpack Configuration in Create React App + +When using Telescope-generated code with Create React App (CRA), you might encounter issues with polyfills and certain modules not being properly resolved due to CRA's webpack configuration restrictions. + +#### Solution + +You need to customize the webpack configuration. Since CRA doesn't allow direct webpack configuration modifications, you can use `react-app-rewired` to override the default configuration: + +1. Install required packages: + +```sh +npm install --save-dev react-app-rewired customize-cra +``` + +2. Create a `config-overrides.js` file in the root of your project: + +```js +const { override, addWebpackAlias, addWebpackPlugin } = require('customize-cra'); +const webpack = require('webpack'); + +module.exports = override( + // Add polyfills and resolve issues + addWebpackPlugin( + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + process: 'process/browser', + }) + ), + addWebpackAlias({ + 'stream': 'stream-browserify', + 'path': 'path-browserify', + 'crypto': 'crypto-browserify', + 'http': 'stream-http', + 'https': 'https-browserify', + 'os': 'os-browserify/browser', + }) +); +``` + +3. Modify your `package.json` scripts to use `react-app-rewired`: + +```json +"scripts": { + "start": "react-app-rewired start", + "build": "react-app-rewired build", + "test": "react-app-rewired test", + "eject": "react-scripts eject" +} +``` + +4. Install the necessary polyfill packages: + +```sh +npm install --save-dev buffer process stream-browserify path-browserify crypto-browserify stream-http https-browserify os-browserify +``` + +### Babel Configuration + +If you're using a custom Babel setup, you might encounter syntax errors with features like numeric separators or optional chaining that are used in Telescope-generated code. + +#### Solution + +Make sure your Babel configuration includes the necessary plugins: + +```sh +npm install --save-dev @babel/plugin-proposal-numeric-separator @babel/plugin-proposal-optional-chaining @babel/plugin-proposal-nullish-coalescing-operator +``` + +Add these plugins to your `.babelrc` or `babel.config.js`: + +```json +{ + "plugins": [ + "@babel/plugin-proposal-numeric-separator", + "@babel/plugin-proposal-optional-chaining", + "@babel/plugin-proposal-nullish-coalescing-operator" + ] +} +``` + +Or if you're using a preset: + +```json +{ + "presets": [ + ["@babel/preset-env", { + "targets": { + "browsers": ["last 2 versions", "not dead"] + } + }] + ] +} +``` + +### Long Type Errors + +When using Telescope-generated code, you might see extremely long and complex TypeScript errors that are difficult to understand. + +#### Solution + +1. Use a specific TypeScript version that works well with CosmJS (4.7.x or later recommended) +2. Simplify your types with type assertions in places where TypeScript is having trouble +3. Add `//@ts-ignore` comments for problematic lines as a last resort +4. Enable the `useDeepPartial` option in your Telescope configuration to simplify partial types + +### Module Resolution Issues + +You might encounter "Cannot find module" errors when importing from Telescope-generated code. + +#### Solution + +Check your `tsconfig.json` to ensure it has the correct module resolution settings: + +```json +{ + "compilerOptions": { + "target": "es2020", + "module": "esnext", + "moduleResolution": "node", + "esModuleInterop": true, + "resolveJsonModule": true, + "baseUrl": "." + } +} +``` + +### Browser Compatibility Issues + +Telescope-generated code uses modern JavaScript features that might not be compatible with older browsers. + +#### Solution + +1. Ensure you have proper polyfills added to your project +2. Configure Babel to target the browsers you need to support +3. Consider using a tool like `core-js` for comprehensive polyfills: + +```sh +npm install core-js +``` + +Then import it at the top of your entry file: + +```js +import 'core-js/stable'; +``` + +### WebAssembly Support + +Some CosmJS dependencies use WebAssembly, which might cause issues in certain environments. + +#### Solution + +Make sure your bundler is configured to handle WebAssembly files (.wasm). For webpack: + +```js +module.exports = { + // ... + experiments: { + asyncWebAssembly: true, + }, +}; +``` + +### Specific Blockchain Issues + +#### Transaction Broadcasting Fails + +If transactions are being rejected by the network: + +1. Ensure account sequence is correct +2. Verify gas settings are appropriate +3. Check that the chain ID matches the network you're connecting to +4. Verify that the RPC endpoint is functioning correctly + +#### Query Errors + +If queries are failing: + +1. Check the RPC or API endpoint URL +2. Ensure the blockchain node is synchronized +3. Verify query parameters are correctly formatted +4. Check for rate limiting issues + +## Debugging Tips + +### Enable Console Logging + +Add more verbose logging to help identify issues: + +```typescript +const client = await SigningStargateClient.connectWithSigner( + rpcEndpoint, + signer, + { + logger: new ConsoleLogger("debug") + } +); +``` + +### Capture Error Details + +Ensure you're capturing and logging full error details: + +```typescript +try { + // Your code +} catch (error) { + console.error("Detailed error:", { + message: error.message, + name: error.name, + stack: error.stack, + details: error.details || "no details", + data: error.data || "no data", + code: error.code || "no code" + }); +} +``` + +### Network Monitoring + +Use browser developer tools to monitor network requests: + +1. Open your browser's developer tools (F12) +2. Go to the Network tab +3. Filter for XHR/Fetch requests +4. Look for requests to RPC endpoints +5. Examine request payloads and response data + +## Getting Help + +If you encounter issues not covered in this document: + +1. Check the [GitHub Issues](https://github.com/hyperweb-io/telescope/issues) for similar problems +2. Ask on the [Cosmos Developer Discord](https://discord.gg/cosmosnetwork) in the #developers channel +3. For Telescope-specific help, reach out in the #telescope channel on Discord +4. [Submit an issue](https://github.com/hyperweb-io/telescope/issues/new) with a complete description, including: + - Error messages + - Telescope version + - Node.js version + - Operating system + - Steps to reproduce the issue \ No newline at end of file diff --git a/docs/types.mdx b/docs/types.mdx index 332dc36372..47aa7b3f4a 100644 --- a/docs/types.mdx +++ b/docs/types.mdx @@ -1,21 +1,192 @@ -## Types +# Types -### Timestamp +Telescope generates TypeScript types from Protocol Buffer definitions for easy integration with TypeScript projects. This page documents the key types generated by Telescope and how to use them. -The representation of `google.protobuf.Timestamp` is configurable by the `prototypes.typingsFormat.timestamp` option. +## Basic Types -| Protobuf type | Default/`date='date'` | `date='timestamp'` | -| --------------------------- | ---------------------- | ----------------------------------| -| `google.protobuf.Timestamp` | `Date` | `{ seconds: Long, nanos: number }`| +| Type Category | Description | Example | +| ------------- | ----------- | ------- | +| Message Types | TypeScript interfaces that match the structure of Protobuf messages | `MsgSend`, `QueryBalanceRequest` | +| Enum Types | TypeScript enums that represent Protobuf enumerations | `BondStatus`, `ResponseCheckTxType` | +| Service Types | Types for RPC service interfaces | `MsgClientImpl`, `QueryClientImpl` | +| Registry Types | Types for registering and managing message types | `GeneratedType`, `Registry` | -TODO +## Message Types -* [ ] add `date='string'` option +For each Protobuf message, Telescope generates several types and helper functions: -### Duration +| Generated Item | Description | Example | +| -------------- | ----------- | ------- | +| Interface | TypeScript interface matching the message structure | `export interface MsgSend { ... }` | +| Constructor Functions | Functions to create message instances | `export const MsgSend = { encode, decode, ... }` | +| Encoding Functions | Functions to serialize/deserialize messages | `encode`, `decode`, `fromPartial`, `fromJSON` | -The representation of `google.protobuf.Duration` is configurable by the `prototypes.typingsFormat.duration` option. +### Example Message Type -| Protobuf type | Default/`duration='duration'` | `duration='string'` | | -| --------------------------- | ---------------------- | ------------------------------------ | ---------------- | -| `google.protobuf.Duration` | `{ seconds: Long, nanos: number }` | `string` | | +```typescript +// Generated interface +export interface MsgSend { + fromAddress: string; + toAddress: string; + amount: Coin[]; +} + +// Generated static methods for the message +export const MsgSend = { + encode(message: MsgSend, writer: Writer = Writer.create()): Writer { ... }, + decode(input: Reader | Uint8Array, length?: number): MsgSend { ... }, + fromJSON(object: any): MsgSend { ... }, + toJSON(message: MsgSend): unknown { ... }, + fromPartial(object: DeepPartial): MsgSend { ... } +} +``` + +## Any Types + +Telescope handles Protobuf's `Any` type, which allows for dynamic typing: + +```typescript +export interface Any { + typeUrl: string; + value: Uint8Array; +} +``` + +### Working with Any Types + +```typescript +import { Any } from "./google/protobuf/any"; +import { MsgSend } from "./cosmos/bank/v1beta1/tx"; + +// Packing a message into Any +const msgSend: MsgSend = { ... }; +const packedMsg = Any.pack(msgSend, "/cosmos.bank.v1beta1.MsgSend"); + +// Unpacking a message from Any (using interfaces) +const unpackedMsg = packedMsg.unpack(MsgSend); +``` + +## Amino Types (for Legacy Compatibility) + +For compatibility with legacy systems (like Keplr), Telescope generates Amino types: + +| Type | Description | Example | +| ---- | ----------- | ------- | +| AminoMsg | Amino-compatible message format for signing | `export interface AminoMsgSend { ... }` | +| AminoConverter | Converts between Amino and native formats | `export const aminoConverter = { ... }` | + +### Example Amino Types + +```typescript +export interface AminoMsgSend { + type: "cosmos-sdk/MsgSend"; + value: { + from_address: string; + to_address: string; + amount: { + denom: string; + amount: string; + }[]; + }; +} + +export const aminoConverters = { + "/cosmos.bank.v1beta1.MsgSend": { + aminoType: "cosmos-sdk/MsgSend", + toAmino: (message: MsgSend): AminoMsgSend["value"] => { ... }, + fromAmino: (object: AminoMsgSend["value"]): MsgSend => { ... } + } +} +``` + +## Registry Types + +Telescope generates registry types for registering message types with various client libraries: + +```typescript +export const registry = [ + ["/cosmos.bank.v1beta1.MsgSend", MsgSend], + ["/cosmos.staking.v1beta1.MsgDelegate", MsgDelegate], + // other message types... +]; + +export const load = (protoRegistry: Registry) => { + registry.forEach(([typeUrl, type]) => { + protoRegistry.register(typeUrl, type as GeneratedType); + }); +}; +``` + +## Custom Type Handling + +Telescope handles special types with custom encoding/decoding: + +| Type | TypeScript Representation | Notes | +| ---- | ------------------------- | ----- | +| `google.protobuf.Timestamp` | `Date` or `{ seconds: Long; nanos: number }` | Configurable via options | +| `google.protobuf.Duration` | `{ seconds: Long; nanos: number }` or `string` | Configurable via options | +| `cosmos.base.v1beta1.Coin` | `{ denom: string; amount: string }` | Represents blockchain tokens | +| `Int64`/`Uint64` | `bigint` or `Long` | Configurable via options | + +## Client Types + +Telescope generates several client types for interacting with blockchain nodes: + +| Client Type | Description | Example Usage | +| ----------- | ----------- | ------------ | +| Query Client | For querying blockchain state | `queryClient.getBalance(...)` | +| Tx Client | For sending transactions | `txClient.send(...)` | +| RPC Client | For low-level RPC calls | `rpcClient.abciQuery(...)` | +| LCD Client | For REST API calls | `lcdClient.cosmos.bank.v1beta1.allBalances(...)` | + +## Type Helper Functions + +| Function | Description | Example | +| -------- | ----------- | ------- | +| `DeepPartial` | Creates a type with all properties of T set to optional | `DeepPartial` | +| `fromPartial` | Creates an object from a partial input | `MsgSend.fromPartial({...})` | +| `toJSON` | Converts a message to a JSON-compatible object | `MsgSend.toJSON(msg)` | +| `fromJSON` | Creates a message from a JSON-compatible object | `MsgSend.fromJSON({...})` | + +## Using Generated Types + +```typescript +import { MsgSend } from "./cosmos/bank/v1beta1/tx"; +import { queryClient, txClient } from "./client"; + +// Create a message +const sendMsg = MsgSend.fromPartial({ + fromAddress: "cosmos1...", + toAddress: "cosmos1...", + amount: [{ denom: "uatom", amount: "1000000" }] +}); + +// Query the blockchain +const balance = await queryClient.cosmos.bank.v1beta1.balance({ + address: "cosmos1...", + denom: "uatom" +}); + +// Send a transaction +const result = await txClient.signAndBroadcast( + [sendMsg], + { gas: "200000", amount: [{ denom: "uatom", amount: "5000" }] } +); +``` + +## Advanced Type Features + +| Feature | Description | +| ------- | ----------- | +| Union Types | Generated for oneOf fields in Protobuf | +| Nested Types | Generated for nested message definitions | +| Global Interface Registry | Manages type information for runtime reflection | +| Type Information | Metadata attached to types for runtime operations | + +## Type Safety Considerations + +- All generated types are strongly typed for TypeScript safety +- Optional fields are properly marked with `?` +- Arrays and repeated fields are correctly typed +- Custom scalar types have proper handling and validation +- Functions include proper type checking \ No newline at end of file diff --git a/learn/_meta.json b/learn/_meta.json new file mode 100644 index 0000000000..c813638a25 --- /dev/null +++ b/learn/_meta.json @@ -0,0 +1,55 @@ +{ + "label": "Learn", + "order": [ + "quickstart", + "programatic-usage", + "options", + "types", + "manually-registering-types", + "composing-messages", + "calculating-fees", + "clients", + "stargate-clients", + "rpc-clients", + "rpc-client-classes", + "lcd-clients", + "lcd-clients-classes", + "json-patch-protos", + "instant-rpc-methods", + "helper-functions-configuration", + "developing", + "creating-signers", + "dependencies", + "cosmwasm", + "broadcasting-messages", + "troubleshooting", + "sponsors", + "stack" + ], + "titles": { + "quickstart": "Quickstart Guide", + "programatic-usage": "Programatic Usage", + "options": "Configuration Options", + "types": "Working with Types", + "manually-registering-types": "Manual Type Registration", + "composing-messages": "Message Composition", + "calculating-fees": "Fee Calculation", + "clients": "Client Overview", + "stargate-clients": "Stargate Clients", + "rpc-clients": "RPC Clients", + "rpc-client-classes": "RPC Client Classes", + "lcd-clients": "LCD Clients", + "lcd-clients-classes": "LCD Client Classes", + "json-patch-protos": "JSON Patch Protos", + "instant-rpc-methods": "Instant RPC Methods", + "helper-functions-configuration": "Helper Functions", + "developing": "Development Guide", + "creating-signers": "Creating Signers", + "dependencies": "Dependencies", + "cosmwasm": "CosmWasm Integration", + "broadcasting-messages": "Message Broadcasting", + "troubleshooting": "Troubleshooting Guide", + "sponsors": "Sponsors", + "stack": "Technology Stack" + } +} \ No newline at end of file diff --git a/learn/advanced-install-and-use.mdx b/learn/advanced-install-and-use.mdx new file mode 100644 index 0000000000..7f96c44523 --- /dev/null +++ b/learn/advanced-install-and-use.mdx @@ -0,0 +1,393 @@ +# Advanced Installation and Usage + +Welcome to this in-depth guide on the various ways to install and use Telescope! In this tutorial, we'll explore all the available installation methods and how to choose the right one for your specific needs. + +## Understanding Your Options + +Telescope offers multiple installation methods, each with its own advantages: + +1. **Telescope CLI** - Direct command-line interface for quick setup +2. **Create Interchain App (CIA)** - Boilerplate generator for full project setup +3. **Create Cosmos App (CCA)** - Alternative boilerplate generator +4. **Manual Installation** - For integrating into existing projects +5. **Programmatic Usage** - For custom build pipelines and advanced configuration + +Let's explore each method to help you decide which is best for your use case. + +## Method 1: Using the Telescope CLI + +The Telescope CLI provides direct access to all Telescope functionality through simple commands. This is ideal for developers who want full control over each step of the process. + +### Step 1: Install the CLI + +First, let's install Telescope globally: + +```sh +npm install -g @cosmology/telescope +``` + +This makes the `telescope` command available in your terminal. + +### Step 2: Generate a Package + +To create a new package, you can use the interactive mode: + +```sh +telescope generate +``` + +This will walk you through a series of questions to configure your package. If you prefer to specify everything in a single command, you can use: + +```sh +telescope generate --access public --userfullname "Your Name" --useremail "your@email.com" --module-desc "Your module description" --username "your-username" --license MIT --module-name "your-module" --chain-name cosmos --use-npm-scoped +``` + +The available options are: +- `--userfullname`: Your full name +- `--useremail`: Your email +- `--module-desc`: Module description +- `--username`: GitHub username +- `--module-name`: Module name +- `--chain-name`: Chain name +- `--access`: Package access (`public` or `private`) +- `--use-npm-scoped`: Use npm scoped package (only works with `--access public`) +- `--license`: License type + +### Step 3: Download Proto Files + +Next, download the protocol buffer files: + +```sh +telescope download +``` + +This command clones repositories into `./git-modules` and generates proto files in `./protos`. You can customize this process with additional options: + +```sh +# Use a config file +telescope download --config ./protod.config.json --out ./git-modules + +# Download from a specific repository +telescope download --git-repo owner/repository --targets cosmos/auth/v1beta1/auth.proto +``` + +### Step 4: Generate TypeScript Code + +Finally, generate TypeScript code from the proto files: + +```sh +telescope transpile +``` + +For custom options, you can use a configuration file: + +```sh +telescope transpile --config your-config.json +``` + +## Method 2: Using Create Interchain App (CIA) + +Create Interchain App provides a streamlined experience with pre-configured boilerplates. This is great for starting new projects quickly. + +### Step 1: Install CIA + +Install the CIA tool globally: + +```sh +npm install -g create-interchain-app +``` + +### Step 2: Create a New Project + +Generate a new project using the Telescope boilerplate: + +```sh +cia --boilerplate telescope +``` + +This creates a project with the necessary configuration files and folder structure already set up for you. + +### Step 3: Navigate to Your Module + +Change to your module directory: + +```sh +cd ./your-project/packages/your-module +yarn install +``` + +### Step 4: Download Proto Files and Generate Code + +The CIA boilerplate includes convenient scripts: + +```sh +# Download proto files +yarn download-protos + +# Generate TypeScript code +yarn codegen +``` + +These scripts handle the complexity of downloading protos and generating code with sensible defaults. + +If you'd like to see this process in action, check out this video tutorial: https://youtu.be/iQf6p65fbdY + +## Method 3: Using Create Cosmos App (CCA) + +Create Cosmos App is another boilerplate generator with a focus on Cosmos ecosystem projects. + +### Step 1: Install CCA + +Install the CCA tool globally: + +```sh +npm install -g create-cosmos-app +``` + +### Step 2: Create a New Package + +Create a new package using the Telescope boilerplate: + +```sh +cca --boilerplate telescope +``` + +### Step 3: Navigate to Your Package + +Move to your package directory: + +```sh +cd ./your-project/packages/telescope +yarn install +``` + +### Step 4: Download Proto Files and Generate Code + +```sh +# Download proto files +telescope download --config ./your.config.json + +# Generate TypeScript code +yarn codegen +``` + +## Method 4: Manual Installation + +If you're working with an existing project and want to integrate Telescope, manual installation might be your best option. + +### Step 1: Add Telescope to Your Project + +```sh +yarn add --dev @cosmology/telescope +``` + +### Step 2: Install Required Dependencies + +```sh +yarn add @cosmjs/amino @cosmjs/proto-signing @cosmjs/stargate @cosmjs/tendermint-rpc +``` + +### Step 3: Use Telescope + +You can use the CLI commands with npm or yarn prefixes: + +```sh +yarn telescope generate +npx telescope download +``` + +Or you can use the programmatic approach described in the next section. + +## Method 5: Programmatic Usage + +For the most flexibility, you can integrate Telescope directly into your build process using its JavaScript API. + +### Step 1: Install Dependencies + +```sh +yarn add --dev @cosmology/telescope +yarn add @cosmjs/amino @cosmjs/proto-signing @cosmjs/stargate @cosmjs/tendermint-rpc +``` + +### Step 2: Create Scripts for Downloading Protos + +Create a file called `download-protos.js` with the following content: + +```js +import downloadProtos from '@cosmology/telescope/main/commands/download' + +const config = { + repos: [ + { owner: "cosmos", repo: "cosmos-sdk", branch: "release/v0.50.x" }, + { owner: "cosmos", repo: "ibc-go" }, + ], + protoDirMapping: { + "gogo/protobuf/master": ".", + "googleapis/googleapis/master": ".", + "protocolbuffers/protobuf/main": "src" + }, + outDir: "protos", + ssh: false, + tempRepoDir: "git-modules", + targets: [ + "cosmos/**/*.proto", + "ibc/**/*.proto", + ] +}; + +downloadProtos(config) + .then(() => console.log('✅ Proto download completed')) + // @ts-ignore + .catch((error) => { + console.error('❌ Proto download failed:', error); + process.exit(1); + }); +``` + +### Step 3: Create Scripts for Code Generation + +Create a file called `codegen.js` with the following content: + +```js +import { join } from 'path'; +import telescope from '@cosmology/telescope'; +import { sync as rimraf } from 'rimraf'; + +// Define input and output paths +const protoDirs = [join(__dirname, '/proto')]; +const outPath = join(__dirname, '/src'); +rimraf(outPath); + +// Run Telescope with options +telescope({ + protoDirs, + outPath, + + // All options are totally optional + options: { + aminoEncoding: { + enabled: true + }, + lcdClients: { + enabled: false + }, + rpcClients: { + enabled: false, + camelCase: true + }, + + // You can scope options to specific packages + packages: { + nebula: { + prototypes: { + typingsFormat: { + useExact: false + } + } + }, + akash: { + stargateClients: { + enabled: true, + includeCosmosDefaultTypes: false + }, + prototypes: { + typingsFormat: { + useExact: false + } + } + } + } + } +}).then(() => { + console.log('✨ All done!'); +}).catch(e => { + console.error('Error generating code:', e); + process.exit(1); +}); +``` + +### Step 4: Integrate with Your Build Process + +Add these scripts to your `package.json`: + +```json +{ + "scripts": { + "download-protos": "node download-protos.js", + "codegen": "node codegen.js", + "build": "npm run download-protos && npm run codegen && tsc" + } +} +``` + +Now you can run `npm run build` to execute the entire process. + +## A Complete Programmatic Example + +Here's a more complete example that includes additional configuration options: + +```js +import { join } from 'path'; +import telescope from '@cosmology/telescope'; +import { sync as rimraf } from 'rimraf'; + +const protoDirs = [join(__dirname, '/proto')]; +const outPath = join(__dirname, '/src/generated'); +rimraf(outPath); + +telescope({ + protoDirs, + outPath, + options: { + tsDisable: { + disableAll: false, + patterns: ['**/amino/**'] + }, + eslintDisable: { + patterns: ['**/tx.amino.ts'] + }, + removeUnusedImports: true, + prototypes: { + includePackageVar: true, + fieldDefaultIsOptional: true, + typingsFormat: { + useDeepPartial: true, + useExact: false, + timestamp: 'date', + duration: 'duration' + } + }, + aminoEncoding: { + enabled: true + }, + lcdClients: { + enabled: true + }, + rpcClients: { + enabled: true, + camelCase: true + } + } +}).then(() => { + console.log('✨ Code generation complete'); +}); +``` + +## Choosing the Right Method + +To help you decide which method to use, here are some recommendations: + +- **For beginners**: Start with Create Interchain App (CIA) for the easiest experience +- **For existing projects**: Use Manual Installation or Programmatic Usage +- **For maximum control**: Use the Telescope CLI directly +- **For custom build pipelines**: Use Programmatic Usage + +## Next Steps + +Now that you understand the various ways to install and use Telescope, you might want to learn more about: + +1. [Configuration Options](./options.mdx) - Deep dive into all the available options +2. [Working with Types](./types.mdx) - How to use the generated TypeScript types +3. [Creating Clients](./stargate-clients.mdx) - How to use the generated clients + +Happy coding! \ No newline at end of file diff --git a/learn/broadcasting-messages.mdx b/learn/broadcasting-messages.mdx new file mode 100644 index 0000000000..b39f6ea368 --- /dev/null +++ b/learn/broadcasting-messages.mdx @@ -0,0 +1,571 @@ +# Broadcasting Messages to the Network + +In this tutorial, we'll learn how to broadcast transactions containing your messages to a Cosmos blockchain. Broadcasting is the final step in the transaction lifecycle, where your signed transaction is sent to the network for processing. + +## Understanding the Transaction Broadcast Process + +When you broadcast a transaction to a Cosmos blockchain, several things happen: + +1. Your transaction is submitted to a node's mempool +2. The node validates your transaction +3. The transaction gets included in a block +4. The transaction is executed by all nodes in the network +5. The transaction's effects are recorded in the blockchain state + +Let's learn how to implement this process in your application. + +## Prerequisites + +Before you can broadcast transactions, you need: + +- A properly configured signer (see [Creating Signers](./creating-signers.md)) +- Messages to send (see [Composing Messages](./composing-messages.md)) +- Transaction fees (see [Calculating Fees](./calculating-fees.md)) + +## Basic Broadcasting with SigningStargateClient + +The most straightforward way to broadcast a transaction is using the `signAndBroadcast` method from a `SigningStargateClient`: + +```typescript +import { SigningStargateClient } from "@cosmjs/stargate"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { registry } from "./codegen/registry"; + +async function broadcastSimpleTransaction() { + // Set up a wallet (in production, use a more secure approach) + const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + "your mnemonic here", // Never hardcode in production! + { prefix: "cosmos" } + ); + + // Get the sender address + const [firstAccount] = await wallet.getAccounts(); + const sender = firstAccount.address; + + // Create a signing client + const client = await SigningStargateClient.connectWithSigner( + "https://rpc.cosmos.network", + wallet, + { registry } + ); + + try { + // Define the messages you want to send + const messages = [ + { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: { + fromAddress: sender, + toAddress: "cosmos1recipient...", + amount: [{ denom: "uatom", amount: "1000000" }] // 1 ATOM + } + } + ]; + + // Define the fee + const fee = { + amount: [{ denom: "uatom", amount: "5000" }], + gas: "200000" + }; + + // Optional memo + const memo = "Transaction sent from my app"; + + // Sign and broadcast in one step + const result = await client.signAndBroadcast( + sender, + messages, + fee, + memo + ); + + console.log("Broadcast result:", result); + + // Check if transaction was successful + if (result.code === 0) { + console.log("Transaction successful!"); + console.log("Transaction hash:", result.transactionHash); + } else { + console.error("Transaction failed with code:", result.code); + console.error("Error message:", result.rawLog); + } + } finally { + client.disconnect(); + } +} + +broadcastSimpleTransaction().catch(console.error); +``` + +## Using Chain-Specific Clients + +Telescope generates chain-specific clients that make broadcasting even easier: + +```typescript +import { getSigningOsmosisClient } from "osmojs"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { MsgSwapExactAmountIn } from "osmojs/types/codegen/osmosis/gamm/v1beta1/tx"; + +async function broadcastWithChainSpecificClient() { + // Create wallet for Osmosis + const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + "your mnemonic here", + { prefix: "osmo" } + ); + + const [account] = await wallet.getAccounts(); + const sender = account.address; + + // Create Osmosis-specific client + const client = await getSigningOsmosisClient({ + rpcEndpoint: "https://rpc.osmosis.zone", + signer: wallet + }); + + try { + // Create a swap message using Telescope-generated types + const swapMsg = { + typeUrl: "/osmosis.gamm.v1beta1.MsgSwapExactAmountIn", + value: MsgSwapExactAmountIn.fromPartial({ + sender: sender, + routes: [ + { + poolId: "1", + tokenOutDenom: "uosmo" + } + ], + tokenIn: { + denom: "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", + amount: "1000000" + }, + tokenOutMinAmount: "500000" + }) + }; + + // Define fee + const fee = { + amount: [{ denom: "uosmo", amount: "5000" }], + gas: "300000" + }; + + // Broadcast the transaction + const result = await client.signAndBroadcast( + sender, + [swapMsg], + fee, + "Swap tokens using Telescope" + ); + + console.log("Transaction hash:", result.transactionHash); + + if (result.code === 0) { + console.log("Swap successful!"); + } else { + console.error("Swap failed:", result.rawLog); + } + } finally { + client.disconnect(); + } +} +``` + +## Simulating Before Broadcasting + +Before sending a transaction, it's often a good idea to simulate it to estimate the required gas: + +```typescript +async function simulateAndBroadcast(client, sender, messages, memo = "") { + try { + // Step 1: Simulate to estimate gas + console.log("Simulating transaction..."); + const gasEstimated = await client.simulate(sender, messages, memo); + console.log("Estimated gas:", gasEstimated); + + // Step 2: Add a safety margin (30%) + const gasLimit = Math.round(gasEstimated * 1.3); + console.log("Gas limit with safety margin:", gasLimit); + + // Step 3: Calculate the fee + const fee = { + amount: [{ denom: "uatom", amount: "5000" }], + gas: gasLimit.toString() + }; + + // Step 4: Sign and broadcast with estimated gas + console.log("Broadcasting transaction..."); + const result = await client.signAndBroadcast(sender, messages, fee, memo); + + console.log("Transaction successful!"); + console.log("Transaction hash:", result.transactionHash); + return result; + } catch (error) { + console.error("Error in transaction:", error.message); + throw error; + } +} +``` + +## Understanding Broadcast Modes + +Under the hood, CosmJS offers different broadcast modes that determine how the client waits for transaction confirmation: + +1. **Block Mode**: Waits for the transaction to be included in a block (default) +2. **Sync Mode**: Returns after the transaction passes mempool validation +3. **Async Mode**: Returns immediately without waiting + +Most of the time, you'll want to use the default Block Mode, but for specialized applications you might use others. + +```typescript +// Lower-level direct broadcast with specific mode (usually not needed) +const result = await client.broadcastTx( + signedTxBytes, + timeoutMs, + "sync" // broadcast mode: "block", "sync", or "async" +); +``` + +## Handling Common Broadcasting Errors + +Let's look at common errors you might encounter when broadcasting: + +```typescript +async function broadcastWithErrorHandling(client, sender, messages, fee, memo = "") { + try { + const result = await client.signAndBroadcast(sender, messages, fee, memo); + + if (result.code === 0) { + return result; // Success + } else { + // Transaction was included in a block but failed execution + if (result.rawLog.includes("insufficient funds")) { + throw new Error("Account doesn't have enough tokens"); + } else if (result.rawLog.includes("out of gas")) { + // Retry with more gas + console.log("Transaction ran out of gas, retrying with more gas..."); + fee.gas = (parseInt(fee.gas) * 1.5).toString(); + return broadcastWithErrorHandling(client, sender, messages, fee, memo); + } else { + throw new Error(`Transaction failed: ${result.rawLog}`); + } + } + } catch (error) { + // Error before transaction was included in a block + if (error.message.includes("account sequence mismatch")) { + // Handle sequence mismatch + console.log("Sequence mismatch, retrying with refreshed account data..."); + const account = await client.getAccount(sender); + // Now you would need to update the sequence and retry + // This is handled automatically by signAndBroadcast, so this error + // usually occurs when manually signing + } else if (error.message.includes("timed out")) { + console.log("Network timeout, retrying..."); + // Wait a bit and retry + await new Promise(resolve => setTimeout(resolve, 2000)); + return broadcastWithErrorHandling(client, sender, messages, fee, memo); + } else { + throw error; // Rethrow other errors + } + } +} +``` + +## Implementing Retry Logic + +For production applications, it's important to implement retry logic to handle transient failures: + +```typescript +async function broadcastWithRetry(client, sender, messages, fee, memo = "", maxRetries = 3) { + let attempt = 0; + + while (attempt < maxRetries) { + try { + attempt++; + console.log(`Attempt ${attempt} of ${maxRetries}...`); + + const result = await client.signAndBroadcast(sender, messages, fee, memo); + + if (result.code === 0) { + console.log("Transaction successful!"); + return result; + } else { + console.error(`Transaction failed with code ${result.code}: ${result.rawLog}`); + + // Check for fee-related errors + if (result.rawLog.includes("insufficient fee")) { + console.log("Increasing fee for next attempt..."); + // Increase fee by 50% + fee.amount[0].amount = (parseInt(fee.amount[0].amount) * 1.5).toString(); + } + // Check for gas-related errors + else if (result.rawLog.includes("out of gas")) { + console.log("Increasing gas limit for next attempt..."); + // Increase gas by 50% + fee.gas = (parseInt(fee.gas) * 1.5).toString(); + } + // If it's another type of error, we might not want to retry + else { + throw new Error(`Transaction failed: ${result.rawLog}`); + } + } + } catch (error) { + console.error(`Attempt ${attempt} failed:`, error.message); + + // Only retry on specific errors + if (error.message.includes("timed out") || + error.message.includes("socket hang up") || + error.message.includes("account sequence mismatch")) { + // Add exponential backoff + const backoffMs = 1000 * Math.pow(2, attempt - 1); + console.log(`Waiting ${backoffMs}ms before retrying...`); + await new Promise(resolve => setTimeout(resolve, backoffMs)); + } else { + throw error; // Don't retry on other errors + } + } + } + + throw new Error(`Failed after ${maxRetries} attempts`); +} +``` + +## Separating Signing and Broadcasting + +For some advanced use cases, you might want to separate the signing and broadcasting steps: + +```typescript +async function signThenBroadcast() { + // Create wallet and client + const wallet = await DirectSecp256k1HdWallet.fromMnemonic("..."); + const [account] = await wallet.getAccounts(); + const sender = account.address; + + // Get chain info + const client = await SigningStargateClient.connectWithSigner("https://rpc...", wallet); + const chainId = await client.getChainId(); + + // Prepare transaction + const messages = [/* your messages here */]; + const fee = { + amount: [{ denom: "uatom", amount: "5000" }], + gas: "200000" + }; + const memo = "Signed offline"; + + // Step 1: Get account info + const accountOnChain = await client.getAccount(sender); + if (!accountOnChain) { + throw new Error("Account not found on chain"); + } + + // Step 2: Prepare the signing document + const signDoc = { + chainId, + accountNumber: accountOnChain.accountNumber, + sequence: accountOnChain.sequence, + fee, + msgs: messages, + memo + }; + + // Step 3: Sign the transaction (can be done offline) + const { signed, signature } = await wallet.signDirect( + sender, + signDoc + ); + + // Step 4: Encode the signed transaction + const signedTx = { + authInfoBytes: signed.authInfoBytes, + bodyBytes: signed.bodyBytes, + signatures: [signature.signature] + }; + + // Step 5: Broadcast the signed transaction + const result = await client.broadcastTx( + signedTx, + 60000, // timeout in ms + "block" // wait for block inclusion + ); + + console.log("Transaction result:", result); +} +``` + +## Working with Transaction Events + +After broadcasting a transaction, you can extract important information from the events: + +```typescript +function parseTransactionEvents(result) { + if (!result.events || result.events.length === 0) { + console.log("No events in transaction"); + return; + } + + console.log("Transaction events:"); + + // Process each event + result.events.forEach(event => { + console.log(`Event type: ${event.type}`); + + // Process attributes within each event + event.attributes.forEach(attr => { + console.log(` ${attr.key}: ${attr.value}`); + }); + + // Extract specific information based on event type + if (event.type === "transfer") { + const recipient = event.attributes.find(a => a.key === "recipient")?.value; + const amount = event.attributes.find(a => a.key === "amount")?.value; + + if (recipient && amount) { + console.log(` Transferred ${amount} to ${recipient}`); + } + } else if (event.type === "message") { + const action = event.attributes.find(a => a.key === "action")?.value; + if (action) { + console.log(` Action performed: ${action}`); + } + } + }); +} + +// After broadcasting +const result = await client.signAndBroadcast(sender, messages, fee, memo); +parseTransactionEvents(result); +``` + +## Best Practices for Production Applications + +When broadcasting transactions in production: + +1. **Always handle errors gracefully** and provide meaningful feedback to users +2. **Implement retry logic** with exponential backoff for transient failures +3. **Simulate transactions first** to get accurate gas estimates +4. **Add a safety margin** to gas estimates (usually 30% is sufficient) +5. **Store transaction hashes** for later reference and to avoid duplicates +6. **Parse transaction events** to confirm the specific actions were performed +7. **Monitor mempool congestion** and adjust gas prices accordingly +8. **Implement a queue system** for high-volume broadcasting scenarios +9. **Track pending transactions** so users can see their status +10. **Implement proper error messages** for different failure scenarios + +## A Complete Broadcasting Example + +Let's put everything together in a complete example: + +```typescript +import { SigningStargateClient, GasPrice, calculateFee } from "@cosmjs/stargate"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { registry } from "./codegen/registry"; +import { MsgSend } from "./codegen/cosmos/bank/v1beta1/tx"; + +async function sendTokens( + mnemonic: string, + recipientAddress: string, + amount: string, + denom: string +) { + try { + console.log("Preparing transaction..."); + + // Create wallet + const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + mnemonic, + { prefix: "cosmos" } + ); + + const [account] = await wallet.getAccounts(); + const sender = account.address; + console.log(`Sender address: ${sender}`); + + // Create client + const rpcEndpoint = "https://rpc.cosmos.network"; + const gasPrice = GasPrice.fromString("0.025uatom"); + + const client = await SigningStargateClient.connectWithSigner( + rpcEndpoint, + wallet, + { + registry, + gasPrice + } + ); + + // Get chain info + const chainId = await client.getChainId(); + console.log(`Connected to chain: ${chainId}`); + + // Check balance + const balance = await client.getBalance(sender, denom); + console.log(`Sender balance: ${balance.amount} ${balance.denom}`); + + const amountToSend = amount; + if (parseInt(balance.amount) < parseInt(amountToSend)) { + throw new Error(`Insufficient balance: ${balance.amount} ${balance.denom}`); + } + + // Create the message + const message = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: MsgSend.fromPartial({ + fromAddress: sender, + toAddress: recipientAddress, + amount: [{ denom, amount: amountToSend }] + }) + }; + + // Simulate transaction to estimate gas + console.log("Simulating transaction to estimate gas..."); + const gasEstimated = await client.simulate(sender, [message], ""); + console.log(`Gas estimate: ${gasEstimated}`); + + // Add safety margin + const gasLimit = Math.round(gasEstimated * 1.3); + + // Calculate fee based on gas limit and gas price + const fee = calculateFee(gasLimit, gasPrice); + console.log(`Fee: ${fee.amount[0].amount} ${fee.amount[0].denom}`); + + // Broadcast with retry logic + console.log("Broadcasting transaction..."); + const result = await broadcastWithRetry( + client, + sender, + [message], + fee, + `Send ${amountToSend} ${denom} to ${recipientAddress}` + ); + + console.log("Transaction successful!"); + console.log(`Transaction hash: ${result.transactionHash}`); + console.log(`Gas used: ${result.gasUsed} / ${result.gasWanted}`); + + // Parse events + parseTransactionEvents(result); + + return result; + } catch (error) { + console.error("Error sending tokens:", error.message); + throw error; + } +} + +// Usage example +sendTokens( + "your mnemonic", + "cosmos1recipient...", + "1000000", // 1 ATOM in uatom + "uatom" +).catch(console.error); +``` + +## Next Steps + +Now that you've learned how to broadcast messages to Cosmos blockchains, you may want to explore: + +- [LCD Clients](./lcd-clients.md) for querying blockchain data via REST +- [RPC Clients](./rpc-clients.md) for more direct interaction with nodes +- [CosmWasm Integration](./cosmwasm.md) if you're working with smart contracts + +By mastering the broadcasting process, you now have the complete toolkit to interact with any Cosmos SDK blockchain using Telescope-generated types and clients. \ No newline at end of file diff --git a/learn/calculating-fees.mdx b/learn/calculating-fees.mdx new file mode 100644 index 0000000000..42fb055374 --- /dev/null +++ b/learn/calculating-fees.mdx @@ -0,0 +1,428 @@ +# How to Calculate Transaction Fees + +In this tutorial, we'll learn how to calculate and manage transaction fees when sending transactions to a Cosmos SDK blockchain using Telescope-generated types. Understanding fees is crucial for building reliable applications that interact with blockchains. + +## Understanding Blockchain Fees + +Every transaction in a Cosmos SDK blockchain requires a fee to be processed. This fee serves two purposes: + +1. Compensates validators for executing and validating your transaction +2. Prevents spam or denial of service attacks on the network + +Unlike fixed fees you might be familiar with in traditional payment systems, blockchain fees are variable and depend on the computational complexity of your transaction. + +## The Components of a Fee + +A fee in Cosmos SDK chains consists of two main components: + +### 1. Gas + +Gas is a measure of computational resources required to process your transaction. Each operation (like sending tokens, delegating, or submitting a proposal) requires different amounts of gas. Think of gas as the "fuel" your transaction needs to run. + +### 2. Gas Price + +Gas price is how much you're willing to pay per unit of gas. It's usually denominated in a specific token (like ATOM, OSMO, etc.). When the network is congested, you might want to pay a higher gas price to prioritize your transaction. + +The total fee is calculated as: +``` +Fee = Gas × Gas Price +``` + +## Basic Fee Structure + +Let's look at how a fee is structured in the code: + +```typescript +// A simple fee structure +const fee = { + amount: [ + { denom: "uatom", amount: "5000" } // 0.005 ATOM (in microatoms) + ], + gas: "200000" // 200,000 units of gas +}; +``` + +The `amount` field specifies how much you're willing to pay in total, and the `gas` field specifies the gas limit for your transaction. + +## Methods for Setting Fees + +There are three main approaches to setting fees: + +### 1. Manual Fee Setting + +This is the simplest approach, where you explicitly specify the gas limit and fee amount: + +```typescript +// Manually set fees +const fee = { + amount: [{ denom: "uatom", amount: "5000" }], + gas: "200000" +}; + +// Use this fee when sending a transaction +const result = await txClient.signAndBroadcast( + sender, + messages, + fee +); +``` + +This approach works well for simple transactions where you know the approximate gas requirements. However, it's less flexible for complex or variable transactions. + +### 2. Fee Calculation with Gas Price + +A more flexible approach is to use a gas price and calculate the fee based on the estimated gas: + +```typescript +import { calculateFee, GasPrice } from "@cosmjs/stargate"; + +// Define a gas price (e.g., 0.025 microatoms per gas unit) +const gasPrice = GasPrice.fromString("0.025uatom"); + +// For a transaction that needs 200,000 gas +const fee = calculateFee(200000, gasPrice); + +// Result will be: +// { +// amount: [{ denom: "uatom", amount: "5000" }], // 200,000 * 0.025 = 5,000 +// gas: "200000" +// } +``` + +This approach allows you to adjust the gas limit while maintaining a consistent price per unit of gas. + +### 3. Automatic Fee Estimation + +The most advanced approach is to automatically estimate the gas needed for your transaction: + +```typescript +import { SigningStargateClient } from "@cosmjs/stargate"; +import { GasPrice } from "@cosmjs/stargate"; + +// Create a client with auto-gas estimation +const client = await SigningStargateClient.connectWithSigner( + rpcEndpoint, + wallet, + { + gasPrice: GasPrice.fromString("0.025uatom"), + // Optional gas adjustment factor (e.g., add 30% margin) + gasAdjustment: 1.3 + } +); + +// The "auto" string tells the client to estimate gas +const result = await client.signAndBroadcast( + sender, + messages, + "auto" +); +``` + +When you use `"auto"`, the client will: +1. Simulate the transaction to estimate the gas required +2. Apply a safety margin (specified by `gasAdjustment`) +3. Calculate the fee based on the adjusted gas estimate and gas price + +## Gas Estimation via Simulation + +Let's look at how gas estimation works in more detail: + +```typescript +import { SigningStargateClient } from "@cosmjs/stargate"; +import { calculateFee, GasPrice } from "@cosmjs/stargate"; + +// First, create a client +const client = await SigningStargateClient.connectWithSigner( + rpcEndpoint, + wallet +); + +// Define messages for your transaction +const messages = [/* your transaction messages */]; + +// Simulate the transaction to estimate gas +const gasEstimate = await client.simulate( + senderAddress, + messages, + "memo" // Optional memo +); + +console.log(`Estimated gas: ${gasEstimate}`); + +// Apply a safety margin (usually 1.3x to 1.5x) +const gasLimit = Math.round(gasEstimate * 1.3); +console.log(`Gas limit with safety margin: ${gasLimit}`); + +// Calculate the fee with a specific gas price +const gasPrice = GasPrice.fromString("0.025uatom"); +const fee = calculateFee(gasLimit, gasPrice); + +console.log("Calculated fee:", fee); +// Now use this fee when sending the transaction +const result = await client.signAndBroadcast( + senderAddress, + messages, + fee +); +``` + +This approach gives you precise control over the fee calculation process while ensuring your transaction has enough gas to complete. + +## Fee Estimation with Telescope-generated Clients + +Telescope generates client code that makes fee handling even easier: + +```typescript +import { createTxClient } from "./tx"; +import { GasPrice } from "@cosmjs/stargate"; + +// Create a transaction client +const txClient = await createTxClient({ + signer: wallet, + rpcEndpoint: "https://rpc.cosmos.network", + gasPrice: GasPrice.fromString("0.025uatom") +}); + +// Define your messages +const messages = [/* your transaction messages */]; + +// Method 1: Let the client handle everything +const result1 = await txClient.signAndBroadcast( + senderAddress, + messages, + "auto" // Automatic gas estimation +); + +// Method 2: Simulate first, then calculate fee manually +const gasEstimate = await txClient.simulate({ + messages, + signer: senderAddress +}); + +const gasLimit = Math.round(gasEstimate * 1.3); +const fee = { + amount: [{ denom: "uatom", amount: (gasLimit * 0.025).toString() }], + gas: gasLimit.toString() +}; + +const result2 = await txClient.signAndBroadcast( + senderAddress, + messages, + fee +); +``` + +## Choosing the Right Fee Denomination + +Most Cosmos chains accept fees in multiple denominations. Here's how to select an appropriate fee denomination based on the user's available balances: + +```typescript +import { createRPCQueryClient } from "./rpc.query"; + +// Create a query client +const queryClient = await createRPCQueryClient({ + rpcEndpoint: "https://rpc.cosmos.network" +}); + +// Get the user's balances +const { balances } = await queryClient.cosmos.bank.v1beta1.allBalances({ + address: userAddress +}); + +// Function to select an appropriate fee denomination +function selectFeeDenom(balances, preferredDenom = "uatom", minimumAmount = 5000) { + // First try the preferred denomination + const preferred = balances.find(coin => coin.denom === preferredDenom); + if (preferred && parseInt(preferred.amount) > minimumAmount) { + return { + denom: preferredDenom, + available: preferred.amount + }; + } + + // If preferred denomination isn't available or sufficient, look for alternatives + // List of accepted fee denominations in order of preference + const acceptedDenoms = ["uatom", "uosmo", "ujuno", "usomm"]; + + for (const denom of acceptedDenoms) { + if (denom === preferredDenom) continue; // Already checked + + const coin = balances.find(c => c.denom === denom); + if (coin && parseInt(coin.amount) > minimumAmount) { + return { + denom, + available: coin.amount + }; + } + } + + // If no good options found, return the preferred denom anyway + // (the transaction will fail if there are insufficient funds) + return { + denom: preferredDenom, + available: "0" + }; +} + +// Use the function to select a fee denomination +const { denom, available } = selectFeeDenom(balances); +console.log(`Using ${denom} for fees. Available: ${available}`); + +// Create a gas price with the selected denomination +const gasPrice = GasPrice.fromString(`0.025${denom}`); +``` + +## Handling Common Fee-related Errors + +Let's look at how to handle common fee-related errors: + +### 1. Insufficient Funds for Fees + +```typescript +try { + const result = await txClient.signAndBroadcast( + senderAddress, + messages, + fee + ); + // Process result +} catch (error) { + if (error.message.includes("insufficient funds")) { + console.error("Account doesn't have enough funds to pay for fees."); + // Maybe suggest getting more tokens or using a different denom + } else { + console.error("Transaction failed:", error); + } +} +``` + +### 2. Out of Gas Errors + +```typescript +try { + const result = await txClient.signAndBroadcast( + senderAddress, + messages, + fee + ); + // Process result +} catch (error) { + if (error.message.includes("out of gas")) { + console.error("Transaction ran out of gas. Trying again with more gas..."); + + // Increase gas limit by 50% + const newGasLimit = Math.round(parseInt(fee.gas) * 1.5); + const newFee = { + amount: [{ + denom: fee.amount[0].denom, + amount: (parseInt(fee.amount[0].amount) * 1.5).toString() + }], + gas: newGasLimit.toString() + }; + + // Retry with higher gas + try { + const retryResult = await txClient.signAndBroadcast( + senderAddress, + messages, + newFee + ); + console.log("Transaction successful with increased gas:", retryResult); + } catch (retryError) { + console.error("Retry also failed:", retryError); + } + } else { + console.error("Transaction failed:", error); + } +} +``` + +## Typical Gas Costs for Common Operations + +Here are approximate gas costs for common operations (these can vary by chain and version): + +```typescript +const GAS_ESTIMATES = { + send: 80000, // Simple token transfer + delegate: 160000, // Delegate tokens to a validator + undelegate: 180000, // Undelegate tokens + redelegate: 240000, // Redelegate tokens from one validator to another + withdrawRewards: 200000, // Withdraw staking rewards + vote: 120000, // Vote on a governance proposal + submitProposal: 250000 // Submit a governance proposal (basic text proposal) +}; + +// Use these estimates for simple cases +const fee = { + amount: [{ denom: "uatom", amount: "5000" }], + gas: GAS_ESTIMATES.send.toString() +}; +``` + +Remember that these are only estimates and might need adjustment based on the specific blockchain and transaction details. + +## Fee Grants + +Some Cosmos chains support fee grants, which allow one account to pay fees on behalf of another. This is useful for dApps that want to cover transaction fees for their users: + +```typescript +import { MsgGrantAllowance } from "./cosmos/feegrant/v1beta1/tx"; +import { BasicAllowance } from "./cosmos/feegrant/v1beta1/feegrant"; +import { Any } from "./google/protobuf/any"; + +// Create a fee allowance that expires in one month +const expirationDate = new Date(); +expirationDate.setMonth(expirationDate.getMonth() + 1); + +// Create a basic allowance +const basicAllowance = BasicAllowance.fromPartial({ + spendLimit: [{ denom: "uatom", amount: "1000000" }], // 1 ATOM limit + expiration: expirationDate // Expires in one month +}); + +// Pack the allowance into an Any type +const packedAllowance = Any.pack( + basicAllowance, + "/cosmos.feegrant.v1beta1.BasicAllowance" +); + +// Create the grant message +const grantMsg = MsgGrantAllowance.fromPartial({ + granter: "cosmos1granter...", // Account paying for fees + grantee: "cosmos1grantee...", // Account using the fees + allowance: packedAllowance +}); + +// Send the grant message +const result = await txClient.signAndBroadcast( + granterAddress, + [grantMsg], + fee +); + +// Now the grantee can send transactions with the granter paying the fees +``` + +After setting up a fee grant, the grantee can send transactions without having tokens for fees. The chain will automatically use the granted allowance. + +## Best Practices + +1. **Always use gas estimation for complex transactions**: Simple transactions like token transfers might work fine with fixed gas, but complex messages need proper estimation. + +2. **Add a safety margin**: Always add 30-50% more gas than estimated to account for blockchain state changes that might occur between simulation and execution. + +3. **Check balances before sending**: Always verify the user has enough balance to cover the fees. + +4. **Handle fee-related errors gracefully**: Implement proper error handling for common fee-related issues. + +5. **Consider network congestion**: During high network activity, you might need to increase gas prices to ensure timely processing. + +6. **Use appropriate denominations**: Make sure you're using fee denominations accepted by validators on that chain. + +7. **Test on testnet first**: Always test your fee handling logic on testnets before deploying to mainnet. + +## Conclusion + +You now understand how to calculate and manage transaction fees when working with Cosmos SDK blockchains using Telescope-generated types. Proper fee handling is crucial for building reliable blockchain applications that provide a good user experience. + +In the next tutorial, we'll learn about [Stargate clients](./stargate-clients.md) for interacting with blockchain nodes. \ No newline at end of file diff --git a/learn/composing-messages.mdx b/learn/composing-messages.mdx new file mode 100644 index 0000000000..a7b631139a --- /dev/null +++ b/learn/composing-messages.mdx @@ -0,0 +1,344 @@ +# Composing and Sending Messages + +In this tutorial, we'll learn how to compose and send messages to a Cosmos blockchain using Telescope-generated types. Messages are the primary way to interact with a blockchain, instructing it to perform operations like transferring tokens, delegating stake, or submitting governance proposals. + +## Understanding Blockchain Messages + +In Cosmos SDK chains, all on-chain actions happen through messages. Think of these messages as instructions that tell the blockchain what you want to do. When these messages are included in a transaction and that transaction is validated and executed, they change the state of the blockchain. + +Telescope generates TypeScript types for these messages from the chain's Protocol Buffer definitions, making it easy to compose valid messages in a type-safe way. + +## Basic Message Structure + +Let's start with the most common message type: sending tokens from one address to another. + +```typescript +import { MsgSend } from "./cosmos/bank/v1beta1/tx"; + +// Create a bank send message +const sendMsg = MsgSend.fromPartial({ + fromAddress: "cosmos1sender...", + toAddress: "cosmos1recipient...", + amount: [ + { denom: "uatom", amount: "1000000" } // 1 ATOM (in microatoms) + ] +}); +``` + +The structure of this message is simple: +- `fromAddress`: The sender's address +- `toAddress`: The recipient's address +- `amount`: An array of coins to send, each with a denomination and amount + +Notice we're using the `fromPartial()` method. This is a helper method generated by Telescope that properly initializes the message with default values for any fields you omit. + +## Creating Different Types of Messages + +Let's look at how to create messages for some common operations: + +### Delegating Stake + +```typescript +import { MsgDelegate } from "./cosmos/staking/v1beta1/tx"; + +const delegateMsg = MsgDelegate.fromPartial({ + delegatorAddress: "cosmos1...", + validatorAddress: "cosmosvaloper1...", + amount: { denom: "uatom", amount: "5000000" } // 5 ATOM +}); +``` + +### Voting on a Governance Proposal + +```typescript +import { MsgVote, VoteOption } from "./cosmos/gov/v1beta1/tx"; + +const voteMsg = MsgVote.fromPartial({ + proposalId: 42, // The ID of the proposal + voter: "cosmos1...", + option: VoteOption.VOTE_OPTION_YES // Could also be NO, ABSTAIN, etc. +}); +``` + +### Submitting a Governance Proposal + +```typescript +import { MsgSubmitProposal } from "./cosmos/gov/v1beta1/tx"; +import { TextProposal } from "./cosmos/gov/v1beta1/gov"; +import { Any } from "./google/protobuf/any"; + +// First, create the proposal content +const textProposal = TextProposal.fromPartial({ + title: "My Awesome Proposal", + description: "This proposal aims to improve the network by..." +}); + +// Pack it into an Any type +const packedProposal = Any.pack(textProposal, "/cosmos.gov.v1beta1.TextProposal"); + +// Now create the submit proposal message +const submitProposalMsg = MsgSubmitProposal.fromPartial({ + content: packedProposal, + initialDeposit: [{ denom: "uatom", amount: "10000000" }], // 10 ATOM + proposer: "cosmos1..." +}); +``` + +## Using MessageComposer for Convenience + +Telescope also generates a `MessageComposer` for each module, which provides a more convenient way to create messages: + +```typescript +import { bankComposer } from "./cosmos/bank/v1beta1/tx.composer"; +import { stakingComposer } from "./cosmos/staking/v1beta1/tx.composer"; + +// Same send message as before, but using the composer +const sendMsg = bankComposer.send({ + fromAddress: "cosmos1...", + toAddress: "cosmos1...", + amount: [{ denom: "uatom", amount: "1000000" }] +}); + +// Delegate message using the composer +const delegateMsg = stakingComposer.delegate({ + delegatorAddress: "cosmos1...", + validatorAddress: "cosmosvaloper1...", + amount: { denom: "uatom", amount: "5000000" } +}); +``` + +The message composer automatically adds the correct type URL to each message, which is needed when sending the messages to the blockchain. + +## Combining Multiple Messages in a Transaction + +One powerful feature of Cosmos SDK chains is the ability to include multiple messages in a single transaction. This allows you to perform several operations atomically: + +```typescript +// Create multiple messages +const messages = [ + bankComposer.send({ + fromAddress: "cosmos1...", + toAddress: "cosmos1recipient1...", + amount: [{ denom: "uatom", amount: "1000000" }] + }), + bankComposer.send({ + fromAddress: "cosmos1...", + toAddress: "cosmos1recipient2...", + amount: [{ denom: "uatom", amount: "2000000" }] + }), + stakingComposer.delegate({ + delegatorAddress: "cosmos1...", + validatorAddress: "cosmosvaloper1...", + amount: { denom: "uatom", amount: "5000000" } + }) +]; + +// These messages will be sent in a single transaction +``` + +## Sending Messages to the Blockchain + +Now that you know how to compose messages, let's see how to send them to the blockchain. + +### Using SigningStargateClient + +The most common way to send transactions is using the `SigningStargateClient` from CosmJS: + +```typescript +import { SigningStargateClient } from "@cosmjs/stargate"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { GasPrice } from "@cosmjs/stargate"; +import { registry } from "./registry"; + +// First, set up a wallet (you would normally have a more secure way to handle the mnemonic) +const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + "your mnemonic phrase here", + { prefix: "cosmos" } +); +const [firstAccount] = await wallet.getAccounts(); + +// Create a signing client +const client = await SigningStargateClient.connectWithSigner( + "https://rpc.cosmos.network", // Your RPC endpoint + wallet, + { + registry, // This is the registry from Telescope + gasPrice: GasPrice.fromString("0.025uatom") + } +); + +// Prepare your message +const sendMsg = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: { + fromAddress: firstAccount.address, + toAddress: "cosmos1recipient...", + amount: [{ denom: "uatom", amount: "1000000" }] + } +}; + +// Send the transaction +try { + const result = await client.signAndBroadcast( + firstAccount.address, + [sendMsg], + { + amount: [{ denom: "uatom", amount: "5000" }], + gas: "200000" + } + ); + + console.log("Transaction hash:", result.transactionHash); + console.log("Transaction result:", result); + + if (result.code === 0) { + console.log("Transaction successful!"); + } else { + console.error("Transaction failed:", result.rawLog); + } +} catch (error) { + console.error("Error sending transaction:", error); +} +``` + +### Using the Telescope-generated TxClient + +Telescope also generates a `createTxClient` function that makes it even easier to send transactions: + +```typescript +import { createTxClient } from "./tx"; +import { MsgSend } from "./cosmos/bank/v1beta1/tx"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; + +// Set up a wallet +const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + "your mnemonic phrase here", + { prefix: "cosmos" } +); +const [firstAccount] = await wallet.getAccounts(); + +// Create a transaction client +const txClient = await createTxClient({ + signer: wallet, + rpcEndpoint: "https://rpc.cosmos.network" +}); + +// Create a message +const sendMsg = MsgSend.fromPartial({ + fromAddress: firstAccount.address, + toAddress: "cosmos1recipient...", + amount: [{ denom: "uatom", amount: "1000000" }] +}); + +// Send the transaction +try { + const result = await txClient.signAndBroadcast( + firstAccount.address, + [sendMsg], + { + amount: [{ denom: "uatom", amount: "5000" }], + gas: "200000" + } + ); + + console.log("Transaction successful:", result); +} catch (error) { + console.error("Error sending transaction:", error); +} +``` + +## Working with Legacy Systems: Amino Encoding + +Some wallets and older systems use Amino encoding instead of Protobuf. Telescope generates Amino converters that make it easy to support both formats: + +```typescript +import { AminoTypes } from "@cosmjs/stargate"; +import { aminoConverters } from "./amino/converters"; + +const aminoTypes = new AminoTypes(aminoConverters); + +// Convert a Protobuf message to Amino format +const aminoMsg = aminoTypes.toAmino({ + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: sendMsg +}); + +console.log(aminoMsg); +// Output: +// { +// type: "cosmos-sdk/MsgSend", +// value: { +// from_address: "cosmos1...", +// to_address: "cosmos1...", +// amount: [{ denom: "uatom", amount: "1000000" }] +// } +// } + +// Convert back from Amino to Protobuf +const protoMsg = aminoTypes.fromAmino(aminoMsg); +``` + +## Handling Message Responses + +After sending a transaction, you'll want to handle the response: + +```typescript +try { + const result = await txClient.signAndBroadcast( + firstAccount.address, + [sendMsg], + fee + ); + + // Check if the transaction was successful + if (result.code === 0) { + console.log("Transaction successful!"); + + // For specific message types, you might want to extract data + // For example, if submitting a proposal, get the proposal ID: + if (result.events) { + const submitProposalEvent = result.events.find(event => + event.type === "submit_proposal" + ); + + if (submitProposalEvent) { + const proposalIdAttr = submitProposalEvent.attributes.find(attr => + attr.key === "proposal_id" + ); + + if (proposalIdAttr) { + console.log("Created proposal ID:", proposalIdAttr.value); + } + } + } + } else { + console.error("Transaction failed:", result.rawLog); + } +} catch (error) { + console.error("Error sending transaction:", error); +} +``` + +## Best Practices + +When composing and sending messages: + +1. **Always use `fromPartial()`**: This ensures default values are properly set for omitted fields. + +2. **Validate inputs**: Although Telescope types help with type safety, it's good practice to validate inputs before creating messages, especially when they come from user input. + +3. **Handle errors gracefully**: Network issues, gas estimation errors, and other problems can occur when sending transactions. + +4. **Monitor transaction status**: For important transactions, consider implementing tracking by transaction hash to confirm they were included in a block. + +5. **Consider gas and fees**: Set appropriate gas limits and fees to ensure your transaction is processed promptly. + +6. **Batch related operations**: When possible, group related operations into a single transaction using multiple messages. + +7. **Test on testnets first**: Before sending transactions to mainnet, test your code on a testnet. + +## Conclusion + +You've now learned how to compose messages using Telescope-generated types and send them to a Cosmos blockchain. These skills form the foundation for building applications that interact with any Cosmos SDK blockchain. + +In the next tutorial, you'll learn about [calculating transaction fees](./calculating-fees.md) to ensure your transactions are processed efficiently. \ No newline at end of file diff --git a/learn/cosmwasm.mdx b/learn/cosmwasm.mdx new file mode 100644 index 0000000000..ec45a773f0 --- /dev/null +++ b/learn/cosmwasm.mdx @@ -0,0 +1,593 @@ +# Building TypeScript SDKs for CosmWasm Smart Contracts + +In this tutorial, we'll explore how to use Telescope to generate TypeScript SDKs for your CosmWasm smart contracts. This powerful integration allows you to interact with your contracts using strongly-typed interfaces in your frontend applications, providing a much better developer experience. + +## Understanding CosmWasm and TypeScript Integration + +CosmWasm is a smart contracting platform built for the Cosmos ecosystem. When building dApps that interact with CosmWasm contracts, you typically need to: + +1. Handle contract message formatting +2. Parse contract responses +3. Manage type conversions +4. Create proper queries and transactions + +Telescope, in conjunction with [@cosmwasm/ts-codegen](https://github.com/CosmWasm/ts-codegen), automates this process by generating TypeScript clients from your contract schema files. This provides you with a type-safe way to interact with your contracts, complete with auto-completion in your IDE. + +## Prerequisites + +Before starting, ensure you have: + +1. A CosmWasm smart contract with schema files generated +2. A Telescope project set up +3. Basic knowledge of TypeScript and CosmWasm + +## Setting Up Your Project + +For this tutorial, we'll assume you already have a Telescope project. If not, you can create one: + +```bash +# Install telescope globally if needed +npm install -g @cosmology/telescope + +# Create a new project +mkdir my-cosmwasm-sdk +cd my-cosmwasm-sdk +npm init -y +npm install --save-dev @cosmology/telescope +``` + +Next, you'll need the schema files for your CosmWasm contract. These are typically generated when you build your contract. If you're working with an existing contract, you might find these in the contract's repository. + +Let's create a directory for our sample schema files: + +```bash +mkdir -p schema/cw721 +``` + +For this tutorial, let's assume we're building an SDK for a CW721 (NFT) contract. You would typically have schema files like: + +- `schema/cw721/instantiate_msg.json` +- `schema/cw721/execute_msg.json` +- `schema/cw721/query_msg.json` +- And others... + +## Configuring Telescope for CosmWasm + +Now, let's update your Telescope configuration to generate TypeScript SDKs for your CosmWasm contracts. Create or modify your `telescope.config.js` file: + +```javascript +const { join } = require('path'); + +module.exports = { + protoDirs: [/* your proto directories */], + outPath: join(__dirname, './src/generated'), + options: { + // Existing Telescope options... + + // Add CosmWasm configuration + cosmwasm: { + contracts: [ + { + name: "CW721", + dir: "./schema/cw721" + } + ], + outPath: "./src/contracts" + } + } +}; +``` + +In this configuration: + +- `name`: The name used for the generated TypeScript classes +- `dir`: The path to your contract schema directory +- `outPath`: Where the generated files should be placed + +## Enhanced Configuration for Complex Projects + +For more complex projects, you might want additional configuration options: + +```javascript +cosmwasm: { + contracts: [ + { + name: "CW721Base", + dir: "./schema/cw721", + + // Convert snake_case to camelCase for better TypeScript experience + camelCase: true, + + // Map external types to your preferred imports + customTypes: { + "cosmos.base.v1beta1.Coin": { + module: "@cosmjs/stargate", + type: "Coin" + } + }, + + // Configure React Query hooks + reactQuery: { + enabled: true, + version: 'v4', + // Optional: customize generated hook names + queryKeys: { + prefix: "useCW721" + } + } + }, + + // You can generate SDKs for multiple contracts + { + name: "Marketplace", + dir: "./schema/marketplace" + } + ], + outPath: "./src/contracts" +} +``` + +## Generating Your CosmWasm SDK + +With your configuration in place, run Telescope to generate your TypeScript SDKs: + +```bash +npx telescope +``` + +After running this command, you should see the generated files in your specified `outPath` directory: + +``` +src/contracts/ +├── CW721Base.client.ts +├── CW721Base.message-composer.ts +├── CW721Base.react-query.ts (if enabled) +├── CW721Base.types.ts +├── Marketplace.client.ts +├── Marketplace.message-composer.ts +└── Marketplace.types.ts +``` + +## Understanding the Generated Files + +Let's go through each of the generated files and understand how to use them: + +### Types File + +The `.types.ts` file contains all the TypeScript interfaces for your contract's messages and responses: + +```typescript +// CW721Base.types.ts +export interface CW721BaseReadOnlyInterface { + ownerOf: (params: OwnerOfRequest) => Promise; + approval: (params: ApprovalRequest) => Promise; + approvals: (params: ApprovalsRequest) => Promise; + tokens: (params: TokensRequest) => Promise; + // More query methods... +} + +export interface CW721BaseInterface extends CW721BaseReadOnlyInterface { + transferNft: (params: TransferNftParams) => Promise; + sendNft: (params: SendNftParams) => Promise; + approve: (params: ApproveParams) => Promise; + mint: (params: MintParams) => Promise; + // More execute methods... +} + +export interface OwnerOfRequest { + token_id: string; + include_expired?: boolean; +} + +export interface OwnerOfResponse { + owner: string; + approvals: Approval[]; +} + +// More interfaces... +``` + +This gives you a complete type-safe representation of your contract's API. + +### Client File + +The `.client.ts` file provides a client class for interacting with your contract: + +```typescript +// CW721Base.client.ts +import { CosmWasmClient, SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import { CW721BaseInterface, CW721BaseReadOnlyInterface } from "./CW721Base.types"; + +export class CW721BaseQueryClient implements CW721BaseReadOnlyInterface { + private readonly client: CosmWasmClient; + private readonly contractAddress: string; + + constructor(client: CosmWasmClient, contractAddress: string) { + this.client = client; + this.contractAddress = contractAddress; + } + + ownerOf = async ({ token_id, include_expired }: OwnerOfRequest): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + owner_of: { token_id, include_expired } + }); + }; + + // More query methods... +} + +export class CW721BaseClient extends CW721BaseQueryClient implements CW721BaseInterface { + private readonly client: SigningCosmWasmClient; + + constructor(client: SigningCosmWasmClient, contractAddress: string) { + super(client, contractAddress); + this.client = client; + } + + transferNft = async ({ sender, recipient, token_id }: TransferNftParams): Promise => { + return this.client.executeContract( + sender, + this.contractAddress, + { transfer_nft: { recipient, token_id } }, + "auto" + ); + }; + + // More execute methods... +} +``` + +### Message Composer + +The `.message-composer.ts` file provides a utility for creating contract messages without actually executing them: + +```typescript +// CW721Base.message-composer.ts +export class CW721BaseMessageComposer { + private readonly contractAddress: string; + + constructor(contractAddress: string) { + this.contractAddress = contractAddress; + } + + transferNft = ({ recipient, token_id }: Omit): any => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: { + contractAddress: this.contractAddress, + msg: Buffer.from(JSON.stringify({ transfer_nft: { recipient, token_id } })), + // Note: sender must be filled in by the client + } + }; + }; + + // More message composers... +} +``` + +### React Query Hooks (if enabled) + +If you enabled React Query integration, the `.react-query.ts` file contains hooks for each query and mutation: + +```typescript +// CW721Base.react-query.ts +import { useQuery, useMutation } from "@tanstack/react-query"; +import { CW721BaseQueryClient, CW721BaseClient } from "./CW721Base.client"; + +export const useCW721OwnerOf = ( + client: CW721BaseQueryClient, + params: OwnerOfRequest, + options?: UseQueryOptions +) => { + return useQuery( + ["cw721-owner-of", client.contractAddress, params], + () => client.ownerOf(params), + options + ); +}; + +export const useCW721TransferNft = ( + client: CW721BaseClient, + options?: UseMutationOptions +) => { + return useMutation( + (params) => client.transferNft(params), + options + ); +}; + +// More hooks... +``` + +## Using Your Generated SDK + +Now that you understand the generated files, let's see how to use them in your application: + +### Basic Querying + +```typescript +import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import { CW721BaseQueryClient } from "./contracts/CW721Base.client"; + +async function queryNFTOwner(tokenId: string) { + // Create a CosmosWasm client + const client = await CosmWasmClient.connect("https://rpc.cosmos.network"); + + // Create a query client for your contract + const contractClient = new CW721BaseQueryClient( + client, + "cosmos1..." // Your contract address + ); + + // Query the contract - with full type safety! + const { owner, approvals } = await contractClient.ownerOf({ + token_id: tokenId + }); + + console.log(`Token ${tokenId} is owned by: ${owner}`); + console.log(`It has ${approvals.length} approvals`); + + return owner; +} +``` + +### Executing Transactions + +```typescript +import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { CW721BaseClient } from "./contracts/CW721Base.client"; + +async function transferNFT(tokenId: string, recipient: string) { + // Create a wallet - in a real app, you'd get this from the user + const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + "your mnemonic here", + { prefix: "cosmos" } + ); + const [firstAccount] = await wallet.getAccounts(); + + // Create a signing client + const client = await SigningCosmWasmClient.connectWithSigner( + "https://rpc.cosmos.network", + wallet + ); + + // Create a contract client + const contractClient = new CW721BaseClient( + client, + "cosmos1..." // Your contract address + ); + + // Execute the transfer - with full type safety! + const result = await contractClient.transferNft({ + sender: firstAccount.address, + recipient, + token_id: tokenId + }); + + console.log(`Transfer successful! Transaction hash: ${result.transactionHash}`); + + return result; +} +``` + +### Using Message Composers + +Message composers are useful when you need to include contract messages in a batch transaction: + +```typescript +import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import { CW721BaseMessageComposer } from "./contracts/CW721Base.message-composer"; + +async function batchTransferNFTs(tokenIds: string[], recipient: string) { + // Create a signing client as before + // ... + + // Create a message composer + const composer = new CW721BaseMessageComposer("cosmos1..."); // Contract address + + // Create multiple transfer messages + const messages = tokenIds.map(tokenId => + composer.transferNft({ + recipient, + token_id: tokenId + }) + ); + + // All messages need a sender + const completeMessages = messages.map(msg => ({ + ...msg, + value: { + ...msg.value, + sender: firstAccount.address + } + })); + + // Broadcast as a batch + const result = await client.signAndBroadcast( + firstAccount.address, + completeMessages, + "auto" + ); + + console.log(`Batch transfer successful! Transaction hash: ${result.transactionHash}`); + + return result; +} +``` + +### Using React Query Hooks + +If you've enabled React Query hooks, you can use them in your React components: + +```tsx +import React from 'react'; +import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import { CW721BaseQueryClient } from "./contracts/CW721Base.client"; +import { useCW721OwnerOf } from "./contracts/CW721Base.react-query"; + +function NFTOwnerDisplay({ tokenId }: { tokenId: string }) { + // In a real app, you'd likely use a provider for this + const [client, setClient] = React.useState(null); + + React.useEffect(() => { + async function setupClient() { + const cosmWasmClient = await CosmWasmClient.connect("https://rpc.cosmos.network"); + const newClient = new CW721BaseQueryClient( + cosmWasmClient, + "cosmos1..." // Contract address + ); + setClient(newClient); + } + + setupClient(); + }, []); + + const { data, isLoading, error } = useCW721OwnerOf( + client!, + { token_id: tokenId }, + { enabled: !!client } + ); + + if (!client || isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return ( +
+

NFT Owner Information

+

Token ID: {tokenId}

+

Owner: {data?.owner}

+

Number of approvals: {data?.approvals.length}

+
+ ); +} +``` + +## Advanced Usage: Multiple Contracts + +For more complex applications, you might need to interact with multiple contracts. Telescope makes this easy by letting you generate SDKs for all your contracts: + +```javascript +cosmwasm: { + contracts: [ + { name: "NFT", dir: "./schema/cw721" }, + { name: "Marketplace", dir: "./schema/marketplace" }, + { name: "Staking", dir: "./schema/staking" } + ], + outPath: "./src/contracts" +} +``` + +Then you can use them together: + +```typescript +import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import { NFTClient } from "./contracts/NFT.client"; +import { MarketplaceClient } from "./contracts/Marketplace.client"; + +async function listNFTForSale(tokenId: string, price: number) { + // Set up wallet and signing client + // ... + + // Create clients for both contracts + const nftClient = new NFTClient( + client, + "cosmos1..." // NFT contract address + ); + + const marketplaceClient = new MarketplaceClient( + client, + "cosmos1..." // Marketplace contract address + ); + + // First approve the marketplace to transfer the NFT + await nftClient.approve({ + sender: firstAccount.address, + spender: marketplaceClient.contractAddress, + token_id: tokenId + }); + + // Then list it on the marketplace + const result = await marketplaceClient.listToken({ + sender: firstAccount.address, + token_contract: nftClient.contractAddress, + token_id: tokenId, + price: price.toString() + }); + + console.log(`NFT listed for sale! Transaction hash: ${result.transactionHash}`); + + return result; +} +``` + +## Customizing Type Imports + +If your contract uses custom types or you want to use specific implementations of common types, you can configure this in the `customTypes` section: + +```javascript +{ + name: "MyContract", + dir: "./schema/mycontract", + customTypes: { + // Map Cosmos SDK Coin type to CosmJS implementation + "cosmos.base.v1beta1.Coin": { + module: "@cosmjs/stargate", + type: "Coin" + }, + + // Map a custom type to your own implementation + "MyCustomType": { + module: "../types/custom", + type: "MyImplementation" + } + } +} +``` + +This ensures that your generated code uses the right imports for these types. + +## Best Practices + +When working with CosmWasm TypeScript SDKs, follow these best practices: + +1. **Keep schema files updated**: Regenerate your SDKs whenever your contract schema changes +2. **Use message composers for batch transactions**: They're more efficient than multiple single transactions +3. **Implement proper error handling**: CosmWasm contract errors are returned as strings +4. **Consider using React Query for state management**: It handles caching, refetching, and loading states +5. **Create a client provider**: In React apps, create a provider that gives components access to contract clients + +## Troubleshooting + +Here are some common issues and their solutions: + +### Schema Files Not Found + +If Telescope can't find your schema files, double-check the path in your configuration. The path is relative to where you run the Telescope command. + +### Generated Types Don't Match Contract + +Ensure your schema files are up-to-date with your contract. You might need to rebuild your contract to regenerate schema files. + +### Type Errors in Generated Code + +If you see type errors in the generated code, you might need to customize how certain types are imported using the `customTypes` configuration. + +### React Query Hooks Not Working + +If your React Query hooks aren't working properly, check that you've installed the correct version of React Query that matches your configuration. + +## Conclusion + +In this tutorial, you've learned how to use Telescope to generate TypeScript SDKs for your CosmWasm smart contracts. This powerful integration brings type safety, auto-completion, and a better developer experience to your dApp development workflow. + +By leveraging these generated SDKs, you can: + +1. Query and execute contract operations with full type safety +2. Compose complex messages for batch transactions +3. Use React Query hooks for efficient state management +4. Work with multiple contracts in a type-safe way + +The combination of Telescope and CosmWasm opens up new possibilities for building sophisticated dApps on the Cosmos ecosystem with a modern TypeScript stack. + +In the next tutorial, we'll explore [Helper Functions Configuration](./helper-functions-configuration.md) to see how Telescope can generate custom utility functions for your blockchain interactions. \ No newline at end of file diff --git a/learn/creating-signers.mdx b/learn/creating-signers.mdx new file mode 100644 index 0000000000..02e2d75642 --- /dev/null +++ b/learn/creating-signers.mdx @@ -0,0 +1,340 @@ +# Creating Signers for Transaction Authorization + +In this tutorial, we'll learn how to create and use signers with Telescope-generated clients to authenticate and authorize transactions on Cosmos-based blockchains. + +## Understanding Blockchain Authentication + +Before broadcasting a transaction to a blockchain, you need to prove that you have the authority to perform that action. In Cosmos-based blockchains, this is done by cryptographically signing transactions with a private key. Let's learn how to create and use these signers in your applications. + +## Creating Basic Signers + +Let's start by creating a simple signer that we can use for development purposes. + +### Using a Mnemonic (For Development Only) + +```ts +import { chains } from 'chain-registry'; +import { getOfflineSigner as getOfflineSignerAmino } from 'cosmjs-utils'; + +// WARNING: This is just an example mnemonic - never hardcode mnemonics in production code +const mnemonic = 'unfold client turtle either pilot stock floor glow toward bullet car science'; + +// Get the chain configuration from the chain-registry +const chain = chains.find(({ chain_name }) => chain_name === 'osmosis'); + +// Create an Amino-compatible offline signer +const signer = await getOfflineSignerAmino({ + mnemonic, + chain +}); + +// You can get the accounts from this signer +const accounts = await signer.getAccounts(); +console.log('Account address:', accounts[0].address); +``` + +> ⚠️ **Security Warning**: Never store mnemonics or private keys in your application code. The example above is only for development purposes. In production, always use secure key management systems or wallet integrations. + +## Choosing Between Amino and Proto Signers + +Telescope supports two main signing formats: + +### Amino Signer + +The Amino signer uses the legacy Amino encoding format which is compatible with most wallets like Keplr. This is the recommended approach for client applications: + +```ts +import { getOfflineSigner as getOfflineSignerAmino } from 'cosmjs-utils'; + +const signer = await getOfflineSignerAmino({ + mnemonic, + chain +}); +``` + +### Proto Signer + +The Proto signer uses the newer Protobuf encoding format: + +```ts +import { getOfflineSigner as getOfflineSignerProto } from 'cosmjs-utils'; + +const signer = await getOfflineSignerProto({ + mnemonic, + chain +}); +``` + +For most applications, you should use the Amino signer unless you have specific requirements for using the Proto signer. + +## Integrating with Keplr Wallet + +In browser environments, you can integrate with the Keplr wallet extension to handle signing: + +```ts +// Check if Keplr is available +if (!window.keplr) { + alert("Please install Keplr extension"); + throw new Error("Keplr extension not found"); +} + +// Request access to the specified chain +await window.keplr.enable(chainId); + +// Get the offline signer +const signer = window.keplr.getOfflineSigner(chainId); + +// You can now use this signer with your Telescope client +const client = await getSigningOsmosisClient({ + rpcEndpoint: "https://rpc.osmosis.zone", + signer +}); + +// Get the user's address from the signer +const accounts = await signer.getAccounts(); +const address = accounts[0].address; +console.log("Connected address:", address); +``` + +## Creating a Signing Client with Your Signer + +Once you have a signer, you can create a signing client to interact with the blockchain: + +```ts +import { getSigningOsmosisClient } from 'osmojs'; + +// Create the signing client +const client = await getSigningOsmosisClient({ + rpcEndpoint: "https://rpc.osmosis.zone", + signer: signer +}); + +// Now you can use this client to broadcast transactions +// For example, to send tokens: +const result = await client.sendTokens( + senderAddress, + recipientAddress, + [{ denom: "uosmo", amount: "1000000" }], + fee, + "Transfer tokens" +); + +console.log("Transaction hash:", result.transactionHash); +``` + +## Working with DirectSecp256k1Wallet + +For more control over the signing process, you can use the `DirectSecp256k1Wallet` from CosmJS: + +```ts +import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing'; +import { fromHex } from '@cosmjs/encoding'; + +// Create a wallet from a private key (hex encoded) +const privateKey = fromHex("1234..."); // Your private key here +const wallet = await DirectSecp256k1Wallet.fromKey(privateKey, "osmo"); + +// Or generate a random wallet +const randomWallet = await DirectSecp256k1Wallet.generate("osmo"); +console.log("Random address:", (await randomWallet.getAccounts())[0].address); +console.log("Make sure to save this mnemonic:", await randomWallet.mnemonic); +``` + +## Advanced: Custom Registry Configuration + +When working with custom chains or when you need support for specific message types, you might need to configure the registry and amino types: + +```ts +import { Registry, EncodeObject } from '@cosmjs/proto-signing'; +import { SigningStargateClient, AminoTypes, StdFee } from '@cosmjs/stargate'; +import { + cosmosAminoConverters, cosmosProtoRegistry, + osmosisAminoConverters, osmosisProtoRegistry +} from 'osmojs'; + +// Combine registries from different modules +const registry = new Registry([ + ...cosmosProtoRegistry, + ...osmosisProtoRegistry +]); + +// Combine amino converters from different modules +const aminoTypes = new AminoTypes({ + ...cosmosAminoConverters, + ...osmosisAminoConverters +}); + +// Create a client with custom configuration +const client = await SigningStargateClient.connectWithSigner( + "https://rpc.osmosis.zone", + signer, + { registry, aminoTypes } +); + +// Create a message +const message: EncodeObject = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: { + fromAddress: "osmo1...", + toAddress: "osmo1...", + amount: [{ denom: "uosmo", amount: "1000000" }] + } +}; + +// Define fee +const fee: StdFee = { + amount: [{ denom: "uosmo", amount: "5000" }], + gas: "200000" +}; + +// Broadcast the transaction +const result = await client.signAndBroadcast( + "osmo1...", // sender address + [message], // messages + fee, // fee + "Send tokens via custom client" // memo +); + +console.log("Transaction result:", result); +``` + +## Security Best Practices + +When working with signers and private keys, always follow these security practices: + +1. **Never hardcode mnemonics or private keys** in your application code +2. **Don't store unencrypted private keys** on the client side +3. **Use hardware wallets or browser extensions** like Keplr when possible +4. **Implement proper key rotation** procedures for long-running applications +5. **Use secure key management services** for server-side applications +6. **Apply proper encryption** for any stored key material +7. **Avoid transmitting private keys** over the network + +## Troubleshooting Common Issues + +### Invalid Address Prefix + +If you see address validation errors, ensure you're using the correct chain prefix: + +```ts +// Make sure the prefix matches the chain you're targeting +const wallet = await DirectSecp256k1Wallet.generate("osmo"); // for Osmosis +// or +const wallet = await DirectSecp256k1Wallet.generate("cosmos"); // for Cosmos Hub +``` + +### Transaction Simulation Failures + +If your transactions are failing during simulation: + +```ts +try { + // Try to simulate the transaction first + const gasEstimated = await client.simulate(address, messages, memo); + console.log("Estimated gas:", gasEstimated); + + // Create a fee with a buffer + const fee = { + amount: [{ denom: "uosmo", amount: "5000" }], + gas: Math.ceil(gasEstimated * 1.3).toString() + }; + + // Now broadcast with the calculated fee + const result = await client.signAndBroadcast(address, messages, fee, memo); + console.log("Success:", result); +} catch (error) { + console.error("Simulation failed:", error.message); + // Handle the error appropriately +} +``` + +## Practical Example: Sending Tokens + +Let's put everything together in a practical example of sending tokens: + +```ts +import { getSigningOsmosisClient } from 'osmojs'; +import { getOfflineSigner as getOfflineSignerAmino } from 'cosmjs-utils'; +import { chains } from 'chain-registry'; +import { GasPrice } from '@cosmjs/stargate'; + +async function sendTokens() { + try { + // Get chain info from registry + const chain = chains.find(({ chain_name }) => chain_name === 'osmosis'); + + // Get signer from Keplr or create from mnemonic for development + let signer; + if (window.keplr) { + await window.keplr.enable(chain.chain_id); + signer = window.keplr.getOfflineSigner(chain.chain_id); + } else { + // Dev-only fallback + const mnemonic = 'your test mnemonic here'; + signer = await getOfflineSignerAmino({ mnemonic, chain }); + } + + // Get account address + const accounts = await signer.getAccounts(); + const fromAddress = accounts[0].address; + + // Create signing client + const client = await getSigningOsmosisClient({ + rpcEndpoint: chain.apis.rpc[0].address, + signer: signer + }); + + // Set recipient and amount + const toAddress = 'osmo1recipient...'; + const amount = [{ denom: 'uosmo', amount: '1000000' }]; // 1 OSMO + + // Estimate fee + const gasPrice = GasPrice.fromString('0.025uosmo'); + const gasEstimated = await client.simulate(fromAddress, [ + { + typeUrl: '/cosmos.bank.v1beta1.MsgSend', + value: { + fromAddress: fromAddress, + toAddress: toAddress, + amount: amount + } + } + ], ''); + + const fee = { + amount: [{ denom: 'uosmo', amount: gasPrice.amount.multiply(gasEstimated).toString() }], + gas: gasEstimated.toString() + }; + + // Send tokens + const result = await client.sendTokens( + fromAddress, + toAddress, + amount, + fee, + 'Sent from my application' + ); + + console.log('Transaction successful!'); + console.log('Transaction hash:', result.transactionHash); + + return result; + } catch (error) { + console.error('Error sending tokens:', error); + throw error; + } +} + +// Call the function +sendTokens().catch(console.error); +``` + +## Next Steps + +Now that you've learned how to create and use signers, you're ready to build more complex applications with Telescope. Here are some recommended next steps: + +- Learn about [composing messages](./composing-messages.md) for different transaction types +- Understand how to [calculate fees](./calculating-fees.md) for your transactions +- Explore [Stargate clients](./stargate-clients.md) for more advanced interactions + +Remember to always prioritize security when dealing with private keys and signers in your applications. \ No newline at end of file diff --git a/learn/dependencies.mdx b/learn/dependencies.mdx new file mode 100644 index 0000000000..d58eed7deb --- /dev/null +++ b/learn/dependencies.mdx @@ -0,0 +1,285 @@ +# Setting Up Dependencies for Your Telescope Project + +In this tutorial, we'll walk through the process of setting up and managing dependencies for your Telescope project. You'll learn which packages are necessary for different features and how to configure them properly. + +## Understanding Telescope's Dependency Structure + +When working with Telescope, you'll need two different types of dependencies: + +1. **Development Dependencies**: Used during code generation +2. **Runtime Dependencies**: Used by the generated code in your application + +Let's explore each type and set up a project from scratch. + +## Creating a New Project + +First, let's create a new project directory and initialize a new npm package: + +```bash +mkdir my-cosmos-app +cd my-cosmos-app +npm init -y +``` + +## Setting Up TypeScript + +Since Telescope generates TypeScript code, we'll need to set up TypeScript in our project: + +```bash +npm install --save-dev typescript @types/node +``` + +Create a basic `tsconfig.json` file: + +```bash +npx tsc --init +``` + +Edit the `tsconfig.json` file to include these important settings: + +```json +{ + "compilerOptions": { + "target": "es2020", + "module": "esnext", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +## Installing Telescope as a Development Dependency + +Now, let's install Telescope as a development dependency: + +```bash +npm install --save-dev @cosmology/telescope +``` + +This package is only needed during development to generate code from your proto files. + +## Installing Runtime Dependencies + +The code generated by Telescope requires several CosmJS packages to function correctly. Let's install them: + +```bash +npm install @cosmjs/amino @cosmjs/proto-signing @cosmjs/stargate @cosmjs/tendermint-rpc +``` + +Let's understand what each package does: + +### @cosmjs/amino + +This package implements the Amino encoding format, which is used for transaction signing in Cosmos SDK chains. Amino is an object encoding specification used by the Cosmos SDK to encode data structures for blockchain state. + +```typescript +import { encodeSecp256k1Pubkey } from "@cosmjs/amino"; + +// Example usage +const pubkey = encodeSecp256k1Pubkey(publicKeyBytes); +``` + +### @cosmjs/proto-signing + +This package provides Protobuf-based transaction signing capabilities. It's essential for creating and signing transactions on Cosmos SDK chains that use Protobuf encoding (Stargate and later). + +```typescript +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; + +// Example usage: Creating a wallet from a mnemonic +const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + "your mnemonic phrase here", + { prefix: "cosmos" } +); +``` + +### @cosmjs/stargate + +This is a high-level client for interacting with Cosmos SDK chains that implement the Stargate API. It provides functionality for querying data and broadcasting transactions. + +```typescript +import { StargateClient } from "@cosmjs/stargate"; + +// Example usage: Connecting to a Cosmos SDK chain +const client = await StargateClient.connect("https://rpc.cosmoshub.strange.love"); +const balance = await client.getAllBalances("cosmos1..."); +``` + +### @cosmjs/tendermint-rpc + +This package implements the Tendermint RPC client, which is used for low-level interactions with the Tendermint consensus engine that powers Cosmos SDK chains. + +```typescript +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; + +// Example usage: Connecting to a Tendermint node +const tmClient = await Tendermint34Client.connect("https://rpc.cosmoshub.strange.love"); +const status = await tmClient.status(); +``` + +## Optional Dependencies for Specific Features + +Depending on the features you're using in your application, you might need additional dependencies: + +### For LCD Clients + +If you're using Telescope's LCD client generation: + +```bash +npm install @cosmology/lcd +``` + +This package provides utilities for working with LCD (Light Client Daemon) endpoints. + +### For React Integration + +If you're using React and want to use the React Query hooks that Telescope can generate: + +```bash +npm install @tanstack/react-query +``` + +Then update your Telescope configuration to enable React hooks: + +```javascript +module.exports = { + // ... + options: { + helperFunctions: { + enabled: true, + hooks: { + react: true + } + } + } +}; +``` + +### For Vue Integration + +If you're using Vue: + +```bash +npm install @tanstack/vue-query +``` + +### For CosmWasm Support + +If you're working with CosmWasm smart contracts: + +```bash +npm install @cosmjs/cosmwasm-stargate +``` + +This package extends the Stargate client with CosmWasm-specific functionality. + +## Managing Dependencies in a Larger Project + +As your project grows, it's important to keep your dependencies organized and up-to-date. Here are some tips: + +### Using a package.json Organization Strategy + +Structure your dependencies in package.json by purpose: + +```json +{ + "dependencies": { + "// Cosmos SDK Core": "", + "@cosmjs/amino": "^0.31.1", + "@cosmjs/proto-signing": "^0.31.1", + "@cosmjs/stargate": "^0.31.1", + "@cosmjs/tendermint-rpc": "^0.31.1", + + "// CosmWasm": "", + "@cosmjs/cosmwasm-stargate": "^0.31.1", + + "// UI Framework": "", + "react": "^18.2.0", + "react-dom": "^18.2.0", + + "// State Management": "", + "@tanstack/react-query": "^4.29.5" + }, + "devDependencies": { + "// Code Generation": "", + "@cosmology/telescope": "^1.0.0", + + "// TypeScript": "", + "typescript": "^5.0.4", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0" + } +} +``` + +### Keeping Versions in Sync + +It's important to keep all CosmJS packages at the same version to avoid compatibility issues: + +```bash +npm install @cosmjs/amino@0.31.1 @cosmjs/proto-signing@0.31.1 @cosmjs/stargate@0.31.1 @cosmjs/tendermint-rpc@0.31.1 +``` + +### Using a Version Management Tool + +For more complex projects, consider using a tool like `npm-check-updates` to help manage dependency versions: + +```bash +npm install -g npm-check-updates +ncu -u +npm install +``` + +## Troubleshooting Dependency Issues + +### Missing Peer Dependencies + +If you see warnings about missing peer dependencies, install them explicitly: + +```bash +npm install --save-peer [package-name] +``` + +### Version Conflicts + +If you encounter version conflicts, check which packages are causing the conflict: + +```bash +npm ls @cosmjs/encoding +``` + +Then, ensure all dependent packages are using compatible versions. + +### Browser Polyfills + +When building for browsers, you might need polyfills for Node.js built-in modules: + +```bash +npm install buffer process stream-browserify path-browserify crypto-browserify +``` + +Then configure your bundler to use these polyfills. + +## Creating a Project Template + +For future projects, consider creating a template with all dependencies pre-configured: + +1. Set up a project with all common dependencies +2. Create a basic folder structure +3. Add configuration files (tsconfig.json, telescope.config.js, etc.) +4. Save it as a template or in a repository + +You can then clone or copy this template when starting a new project. + +## Conclusion + +Setting up dependencies correctly is a crucial step in creating a successful Telescope project. By understanding which packages are needed and how they work together, you can create a solid foundation for your Cosmos SDK application. + +In the next tutorial, we'll explore how to configure Telescope to generate code from your proto files and how to use the generated code in your application. \ No newline at end of file diff --git a/learn/developing/creating-new-generators.mdx b/learn/developing/creating-new-generators.mdx new file mode 100644 index 0000000000..d4cd75f2bc --- /dev/null +++ b/learn/developing/creating-new-generators.mdx @@ -0,0 +1,331 @@ +# Creating Your First Generator + +In this tutorial, you'll learn how to create a new generator for Telescope. We'll walk through each step, from finding a home for your code to testing and using your generator. + +## What Are Generators? + +Generators are the core components of Telescope that transform Protocol Buffer definitions into TypeScript code. Creating your own generator allows you to extend Telescope with custom functionality. + +## Let's Build a Generator! + +### Step 1: Find a Home for Your Code + +First, you need to decide where your generator should live within the codebase. Navigate to `packages/ast/src` and choose an appropriate folder. + +Telescope has specific folders for different types of generators: + +``` +packages/ast/src/ +├── bundle # For bundled objects mapping to Go exports +├── clients # For building clients (LCD, RPC, gRPC, etc.) +├── docs # For generating documentation +├── encoding # For Proto or Amino encoding +├── plugins # For looking up options when building plugins +├── registry # For generating the CosmJS message registry +├── state # For state management (React Query, MobX) +└── utils # For other utilities +``` + +For this tutorial, let's create a client generator, so we'll use the `clients` folder: + +```bash +# Create a directory for our new client type +mkdir -p packages/ast/src/clients/custom +``` + +### Step 2: Write the AST Function + +Create a new file `packages/ast/src/clients/custom/my-custom-client.ts`: + +```typescript +import { ProtoService } from "@cosmology/types"; +import { GenericParseContext } from "../../../encoding"; +import * as t from '@babel/types'; + +export const createMyCustomClient = ( + context: GenericParseContext, + service: ProtoService +) => { + // Add any utilities you'll need + context.addUtil("SuperInterface"); + + // Create method properties for each service method + const methodProperties = service.methods.map(method => { + return t.tsPropertySignature( + t.identifier(method.name), + t.tsTypeAnnotation( + t.tsFunctionType( + [ + t.identifier( + `request: ${method.requestType}` + ) + ], + t.tsTypeAnnotation( + t.tsTypeReference( + t.identifier("Promise"), + t.tsTypeParameterInstantiation([ + t.tsTypeReference( + t.identifier(method.responseType) + ) + ]) + ) + ) + ) + ) + ); + }); + + // Create and return the interface + return t.exportNamedDeclaration( + t.tsInterfaceDeclaration( + t.identifier(`${service.name}CustomClient`), + null, + [t.tsExpressionWithTypeArguments(t.identifier('SuperInterface'))], + t.tsInterfaceBody(methodProperties) + ) + ); +}; +``` + +### Step 3: Add it to the Index + +Create or update the index file `packages/ast/src/clients/custom/index.ts`: + +```typescript +export * from './my-custom-client'; +``` + +### Step 4: Write a Test + +Create a test file `packages/ast/src/clients/custom/my-custom-client.test.ts`: + +```typescript +import { ProtoStore, traverse, getNestedProto } from '@cosmology/proto-parser' +import { defaultTelescopeOptions, ProtoService } from '@cosmology/types'; +import { expectCode, getTestProtoStore, printCode } from '../../../../test-utils'; +import { GenericParseContext } from '../../../encoding'; +import { createMyCustomClient } from './my-custom-client'; + +describe('My Custom Client', () => { + it('generates the custom client interface', () => { + // Set up the test environment + const store = getTestProtoStore(); + store.options = { ...defaultTelescopeOptions }; + store.traverseAll(); + + // Find a service to test with + const ref = store.findProto('cosmos/bank/v1beta1/tx.proto'); + const res = traverse(store, ref); + const service: ProtoService = getNestedProto(res).Msg; + const context = new GenericParseContext(ref, store, store.options); + + // For development, uncomment to see the generated code + // printCode(createMyCustomClient(context, service)); + + // Create a snapshot test + expectCode(createMyCustomClient(context, service)); + }); +}); +``` + +### Step 5: Run Your Test + +```bash +# Navigate to the AST package +cd packages/ast + +# Build the package to update dependencies +yarn build + +# Run your test +yarn test -t "My Custom Client" +``` + +### Step 6: Add Options + +Let's add options for our new client. Edit `packages/types/src/telescope.ts`: + +```typescript +interface TelescopeOpts { + // ... existing options + + customClient?: { + enabled: boolean; + // Control which packages to include + include?: { + patterns?: string[]; // e.g., 'osmosis/**/gamm/**/query.proto' + packages?: string[]; // e.g., 'cosmos.bank.v1beta1' + protos?: string[]; // e.g., 'akash/cert/v1beta2/query.proto' + } + }; +} +``` + +Don't forget to rebuild the types package: + +```bash +cd packages/types +yarn build +``` + +### Step 7: Create a Generator + +Now, create a generator in `packages/telescope/src/generators/create-custom-client.ts`: + +```typescript +import { join } from 'path'; +import { TelescopeBuilder } from '../builder'; +import { TelescopeParseContext } from '../contexts'; +import { createMyCustomClient } from '@cosmology/ast/custom'; // Import from your built package +import { aggregateImports, fixlocalpaths, getImportStatements, writeAstToFile } from '@cosmology/utils'; + +export const plugin = ( + builder: TelescopeBuilder +) => { + // Check if the plugin is enabled + if (!builder.options.customClient?.enabled) { + return; + } + + // Define filename + const localname = 'custom-client.ts'; + + // Process each proto file that has services + builder.store.all().forEach(ref => { + const proto = builder.store.get(ref); + + // Skip if no services + if (!proto.services || proto.services.length === 0) { + return; + } + + // Create a context for this proto + const pCtx = new TelescopeParseContext( + ref, + builder.store, + builder.options + ); + + // Generate code for each service + proto.services.forEach(service => { + // Generate the AST + const ast = createMyCustomClient(pCtx, service); + + // Generate imports + const imports = fixlocalpaths(aggregateImports(pCtx, {}, localname)); + const importStmts = getImportStatements(localname, imports); + + // Combine imports and AST + const prog = [].concat(importStmts).concat(ast); + + // Write the file + const filename = join(builder.outPath, service.name.toLowerCase(), localname); + builder.files.push(`${service.name.toLowerCase()}/${localname}`); + + writeAstToFile(builder.outPath, builder.options, prog, filename); + }); + }); +}; +``` + +### Step 8: Add the Generator to the Builder + +Edit `packages/telescope/src/builder.ts`: + +```typescript +import { createCustomClient } from './generators/create-custom-client'; + +export class TelescopeBuilder { + async build() { + // ... existing generators + + // Add your custom client generator + createCustomClient(this); + + // ... rest of the code + } +} +``` + +### Step 9: Test the Full Generation + +Create a test for the complete generator: + +```typescript +// packages/telescope/src/__tests__/custom-client.test.ts +import { TelescopeBuilder } from '../builder'; +import { TelescopeOptions, TelescopeInput } from '@cosmology/types'; + +const outPath = __dirname + '/../../../__fixtures__/v-next/custom-client'; + +describe('Custom Client Generator', () => { + it('should generate custom clients', async () => { + // Create options with our plugin enabled + const options: TelescopeOptions = { + customClient: { + enabled: true + }, + // Add other required options + }; + + // Create the input + const input: TelescopeInput = { + outPath, + protoDirs: [__dirname + '/../../../__fixtures__/chain1'], + options + }; + + // Create and run the builder + const telescope = new TelescopeBuilder(input); + await telescope.build(); + + // Add assertions if needed + }); +}); +``` + +## Using Your Generator + +To use your generator in a project: + +1. Add the options to your Telescope configuration: + +```typescript +// telescope.config.ts +import { TelescopeOptions } from '@cosmology/types'; + +const options: TelescopeOptions = { + // ... other options + customClient: { + enabled: true, + include: { + packages: ['cosmos.bank.v1beta1'] + } + } +}; + +export default options; +``` + +2. Run Telescope: + +```bash +telescope generate --config telescope.config.ts +``` + +## Next Steps + +Now that you've created your first generator, you can: + +1. Add more features to your generator +2. Create generators for different types of code +3. Contribute your generator to the Telescope repository +4. Share your generator with the community + +Remember: +- Follow the existing patterns and code style +- Write tests for your generators +- Document your code and its usage +- Build incrementally, testing as you go + +Congratulations on creating your first Telescope generator! \ No newline at end of file diff --git a/learn/developing/helpers.mdx b/learn/developing/helpers.mdx new file mode 100644 index 0000000000..de5cbe520b --- /dev/null +++ b/learn/developing/helpers.mdx @@ -0,0 +1,204 @@ +# Working with Common Helpers and Utilities + +In this guide, you'll learn how to use and create common helpers in Telescope. These helpers allow you to reuse code and import common utilities. + +## What Are Helpers? + +Helpers are reusable pieces of code that provide common functionality across your generators. Telescope makes it easy to use these helpers through a system that automatically manages imports. + +## Using Helpers in Your Code + +Let's start with a simple example of using a helper: + +### Step 1: Add a Utility in Your Generator + +```typescript +import * as t from '@babel/types'; +import { GenericParseContext } from '../encoding'; + +export const createMyGenerator = ( + context: GenericParseContext +) => { + // Add the useQuery utility from react-query + context.addUtil('useQuery'); + + // Use it in your generated code + // The import will be automatically added! + return t.exportNamedDeclaration( + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier('useCustomQuery'), + t.callExpression( + t.identifier('useQuery'), + [t.stringLiteral('custom-query')] + ) + ) + ]) + ); +}; +``` + +### Step 2: See How It Works + +When your generator runs, Telescope will: +1. Notice that you called `context.addUtil('useQuery')` +2. Look up where `useQuery` comes from (defined in utils/index.ts) +3. Automatically add the import `import { useQuery } from '@tanstack/react-query'` to your generated file + +## Creating Your Own Helper + +Now, let's create a custom helper: + +### Step 1: Create the Helper Content + +Create a file `packages/telescope/src/helpers/custom-helper.ts`: + +```typescript +export const customHelperCode = ` +// A custom helper function +export const formatData = (data: any) => { + return { + ...data, + formattedDate: new Date(data.timestamp).toISOString() + }; +}; +`; +``` + +### Step 2: Export the Helper + +In `packages/telescope/src/helpers/index.ts`, add: + +```typescript +export * from './custom-helper'; +``` + +### Step 3: Register the Helper + +In `packages/telescope/src/generators/create-helpers.ts`, add: + +```typescript +import { customHelperCode } from '../helpers'; + +export const plugin = ( + builder: TelescopeBuilder +) => { + // ... other helpers + + // Add your custom helper + builder.files.push('custom-helper.ts'); + write(builder, 'custom-helper.ts', customHelperCode); +}; +``` + +### Step 4: Set Up the Alias + +In `packages/telescope/src/utils/index.ts`, add: + +```typescript +export const UTILS = { + // ... other utils + formatData: '__custom-helper__' +}; + +export const UTIL_HELPERS = [ + // ... other helpers + '__custom-helper__' +]; +``` + +### Step 5: Use Your Helper + +Now in any generator, you can use: + +```typescript +context.addUtil('formatData'); +``` + +And Telescope will automatically import it from the generated helper file! + +## Understanding Helper Types + +Telescope supports two main types of utilities: + +### 1. Package Imports + +For importing from external packages: + +```typescript +export const UTILS = { + // Import default export: import useQuery from '@tanstack/react-query' + useQuery: '@tanstack/react-query', + + // Import namespace: import * as _m0 from 'protobufjs/minimal' + _m0: { type: 'namespace', path: 'protobufjs/minimal', name: '_m0' } +}; +``` + +### 2. Helper File Imports + +For importing from generated helper files: + +```typescript +export const UTILS = { + // Import from helper file: import { formatData } from './custom-helper' + formatData: '__custom-helper__' +}; + +export const UTIL_HELPERS = [ + '__custom-helper__' +]; +``` + +## Best Practices + +When working with helpers: + +1. **Use Descriptive Names**: Make helper names clear and meaningful +2. **Keep Helpers Focused**: Each helper should do one thing well +3. **Document Helpers**: Add comments to explain what helpers do +4. **Test Helpers**: Write tests for your helpers to ensure they work correctly + +## Example: Creating a React Context Helper + +Let's create a more advanced helper for React context: + +```typescript +// helpers/context.ts +export const contextHelper = ` +import React, { createContext, useContext, useState } from 'react'; + +export function createAppContext(defaultValue: T) { + const Context = createContext(defaultValue); + + function useAppContext() { + return useContext(Context); + } + + function AppContextProvider({ children, value }: { children: React.ReactNode, value: T }) { + return ( + + {children} + + ); + } + + return { + Context, + useAppContext, + AppContextProvider + }; +} +`; +``` + +## Next Steps + +Now that you understand helpers: + +1. Explore the existing helpers in `packages/telescope/src/helpers` +2. Create your own helpers for common patterns +3. Use `context.addUtil()` in your generators +4. Check out the [Creating New Generators](/learn/developing/creating-new-generators) guide to see how to use these helpers in a complete generator + +By using helpers effectively, you'll write more maintainable and reusable code in your Telescope generators! \ No newline at end of file diff --git a/learn/developing/index.mdx b/learn/developing/index.mdx new file mode 100644 index 0000000000..dadb07875a --- /dev/null +++ b/learn/developing/index.mdx @@ -0,0 +1,30 @@ +# Learning to Develop with Telescope + +Welcome to your journey of contributing to Telescope! This guide will walk you through everything you need to know to become a successful Telescope developer. + +## Your Learning Path + +Start with these fundamental guides: + +- [Setting Up Your Environment](/learn/developing/setup) - Get your development environment ready +- [Running Tests](/learn/developing/tests) - Learn how to test your code +- [Using Starship for Testing](/learn/developing/starship-tests) - Master end-to-end testing +- [Understanding Packages](/learn/developing/packages) - Explore how Telescope is structured +- [Working with ASTs](/learn/developing/working-with-asts) - Learn to manipulate Abstract Syntax Trees +- [Using Common Helpers](/learn/developing/helpers) - Leverage existing utilities +- [Creating New Generators](/learn/developing/creating-new-generators) - Build your own code generators +- [Migrating to v1.0](/learn/developing/migration) - Update your code to the latest version +- [Using Tree-Shakable Hooks](/learn/developing/tree-shakable-hooks) - Implement efficient hooks + +## Getting Started + +We recommend starting with the [Setup Guide](/learn/developing/setup) to prepare your environment, then moving on to [Understanding Packages](/learn/developing/packages) to get familiar with the codebase structure. + +## Need Help? + +If you get stuck at any point: +- Check related reference documentation in the [docs section](/telescope/developing) +- Join our community channels for support +- Open an issue on GitHub + +Happy coding! We're excited to see what you'll build with Telescope. \ No newline at end of file diff --git a/learn/developing/migration.mdx b/learn/developing/migration.mdx new file mode 100644 index 0000000000..f60b814e49 --- /dev/null +++ b/learn/developing/migration.mdx @@ -0,0 +1,228 @@ +# Migrating to Telescope v1.0 + +This guide will walk you through migrating your Telescope project to v1.0. We'll explore the changes, understand why they were made, and show you how to update your configuration. + +## Understanding the Changes + +Telescope v1.0 introduces several important changes to improve type safety, performance, and future compatibility. Let's go through each change step by step. + +## Updating Your Configuration + +### Step 1: Recursive Encoding + +**What Changed**: The `aminoEncoding.useRecursiveV2encoding` option has been removed and replaced with `aminoEncoding.useLegacyInlineEncoding`. + +**How to Update**: + +If you were using: +```typescript +aminoEncoding: { + useRecursiveV2encoding: false +} +``` + +Change it to: +```typescript +aminoEncoding: { + useLegacyInlineEncoding: true +} +``` + +**Why the Change**: The new recursive encoding approach is more reliable and produces cleaner code. Eventually, `useLegacyInlineEncoding` will also be deprecated to ensure everyone uses the new logic. + +### Step 2: Interface Handling + +**What Changed**: The default value of `interfaces.enabled` has changed to `true`. + +**How to Update**: + +If you want to keep it disabled (not recommended), explicitly set: +```typescript +interfaces: { + enabled: false +} +``` + +**Why the Change**: Enabling interfaces by default improves the handling of the `Any` type, providing better type safety and developer experience. + +### Step 3: Decimal Types + +**What Changed**: The default value of `prototypes.typingsFormat.customTypes.useCosmosSDKDec` has changed to `true`. + +**How to Update**: + +If you want to keep it disabled (not recommended), explicitly set: +```typescript +prototypes: { + typingsFormat: { + customTypes: { + useCosmosSDKDec: false + } + } +} +``` + +**Why the Change**: Using `Decimal.fromAtomics` provides proper decimal handling, especially for financial calculations where precision is critical. + +### Step 4: BigInt Support + +**What Changed**: The default value of `prototypes.typingsFormat.num64` has changed to `'bigint'` (from `'long'`). + +**How to Update**: + +If you want to keep using the Long library: +```typescript +prototypes: { + typingsFormat: { + num64: 'long' + } +} +``` + +**Why the Change**: BigInt is now natively supported in JavaScript and provides better performance and type safety for handling 64-bit integers. + +### Step 5: Deep Partial + +**What Changed**: The default value of `prototypes.typingsFormat.useDeepPartial` has changed to `false`. + +**How to Update**: + +If you want to keep using DeepPartial: +```typescript +prototypes: { + typingsFormat: { + useDeepPartial: true + } +} +``` + +**Why the Change**: DeepPartial types can lead to unexpected behavior and less strict type checking. The new default encourages more explicit typing. + +### Step 6: Type URLs + +**What Changed**: The default values of `prototypes.addTypeUrlToDecoders` and `prototypes.addTypeUrlToObjects` have changed to `true`. + +**How to Update**: + +If you want to keep them disabled: +```typescript +prototypes: { + addTypeUrlToDecoders: false, + addTypeUrlToObjects: false +} +``` + +**Why the Change**: Type URLs help with type identification, especially when dealing with message serialization and deserialization. + +### Step 7: Prototype Methods + +**What Changed**: The default values of various `prototypes.methods` have changed to `true`. + +**How to Update**: + +If you want to disable specific methods: +```typescript +prototypes: { + methods: { + toAmino: false, + fromAmino: false, + toProto: false, + fromProto: false + } +} +``` + +**Why the Change**: These methods are commonly used for serialization and make Telescope-generated code more compatible with blockchain SDKs. + +## Example: Full Migration + +Here's a complete example of migrating a configuration: + +```typescript +// Before (v0.x) +const options: TelescopeOptions = { + aminoEncoding: { + useRecursiveV2encoding: false + }, + // Other options... +}; + +// After (v1.0) +const options: TelescopeOptions = { + aminoEncoding: { + useLegacyInlineEncoding: true + }, + // Set other options explicitly if you need to override the new defaults + interfaces: { + enabled: true, // new default, can be omitted + }, + prototypes: { + typingsFormat: { + customTypes: { + useCosmosSDKDec: true, // new default, can be omitted + }, + num64: 'bigint', // new default, can be omitted + useDeepPartial: false, // new default, can be omitted + }, + addTypeUrlToDecoders: true, // new default, can be omitted + addTypeUrlToObjects: true, // new default, can be omitted + methods: { + toAmino: true, // new default, can be omitted + fromAmino: true, // new default, can be omitted + toProto: true, // new default, can be omitted + fromProto: true, // new default, can be omitted + } + } +}; +``` + +## Testing Your Migration + +After updating your configuration: + +1. Run Telescope with your updated configuration: + ```bash + telescope generate --config telescope.config.ts + ``` + +2. Check for any warnings or errors in the console output + +3. Run your tests to ensure everything works: + ```bash + yarn test + ``` + +4. Compare the generated code against your previous version to understand the changes + +## Troubleshooting Common Issues + +### Compilation Errors + +If you encounter TypeScript errors: + +```bash +# Clean the output directory +rm -rf ./generated + +# Regenerate with the new configuration +telescope generate --config telescope.config.ts +``` + +### Runtime Errors + +If you encounter runtime errors after migration: + +1. Check for type mismatches (Long vs BigInt) +2. Check serialization/deserialization methods (toAmino/fromAmino) +3. Ensure your application properly handles Any types if interfaces are enabled + +## Next Steps + +After migrating: + +1. Take advantage of the new features like BigInt support +2. Update your application code to use the improved type safety +3. Consider contributing to Telescope by sharing your migration experience +4. Explore the [Working with ASTs](/learn/developing/working-with-asts) guide to learn more about Telescope's internals + +Happy coding with Telescope v1.0! \ No newline at end of file diff --git a/learn/developing/packages.mdx b/learn/developing/packages.mdx new file mode 100644 index 0000000000..17f002b2ae --- /dev/null +++ b/learn/developing/packages.mdx @@ -0,0 +1,165 @@ +# Understanding Telescope's Package Structure + +Learn about Telescope's monorepo structure and how the different packages work together. This guide will help you navigate and work with the codebase effectively. + +## Package Overview + +Telescope is organized as a monorepo with several packages: + +``` +├── ast # AST generation and manipulation +├── babel # Babel transformations +├── lcd # LCD client generation +├── parser # Protocol Buffer parsing +├── telescope # Main CLI and builder +├── test # Testing utilities +├── types # TypeScript types and options +└── utils # Shared utilities +``` + +## Key Packages + +### 1. AST Package (`packages/ast`) + +The AST package handles Abstract Syntax Tree generation and manipulation: + +```ts +// Example AST generation +import { createMessage } from '@cosmology/ast'; + +const message = createMessage({ + name: 'User', + fields: [ + { name: 'id', type: 'string' } + ] +}); +``` + +### 2. Parser Package (`packages/parser`) + +The parser package handles Protocol Buffer parsing: + +```ts +// Example parsing +import { parse } from '@cosmology/parser'; + +const ast = parse(protoContent); +``` + +### 3. Telescope Package (`packages/telescope`) + +The main package containing the CLI and builder: + +```ts +// Example builder usage +import { TelescopeBuilder } from '@cosmology/telescope'; + +const builder = new TelescopeBuilder({ + protoDirs: ['./protos'], + outPath: './generated' +}); +``` + +## Working with Packages + +### 1. Building Packages + +When working with interdependent packages: + +```sh +# Navigate to the package +cd packages/ast + +# Build the package +yarn build + +# Build TypeScript definitions +yarn build:ts +``` + +### 2. Testing Packages + +Run tests for a specific package: + +```sh +# Run tests +yarn test + +# Run tests in watch mode +yarn test:watch +``` + +### 3. Package Dependencies + +Understand package dependencies: + +```json +// package.json +{ + "dependencies": { + "@cosmology/ast": "workspace:*", + "@cosmology/parser": "workspace:*" + } +} +``` + +## Best Practices + +### 1. Package Organization + +- Keep related code together +- Use clear package boundaries +- Follow the existing structure + +### 2. Cross-Package Development + +When making changes across packages: + +1. Make changes in the source package +2. Build the package +3. Test the changes +4. Update dependent packages + +### 3. Version Management + +- Use workspace dependencies +- Keep versions in sync +- Update changelogs + +## Common Patterns + +### 1. Adding a New Package + +1. Create the package directory +2. Initialize package.json +3. Add workspace configuration +4. Set up build process + +### 2. Modifying Existing Packages + +1. Make changes +2. Build the package +3. Update dependent packages +4. Run tests + +### 3. Package Integration + +1. Add dependencies +2. Import and use +3. Test integration +4. Document usage + +## Next Steps + +Now that you understand the package structure, you can: + +1. Navigate the codebase +2. Make changes to packages +3. Add new packages +4. Contribute effectively + +Remember: +- Keep packages focused +- Maintain clear boundaries +- Follow the build process +- Test thoroughly \ No newline at end of file diff --git a/learn/developing/setup.mdx b/learn/developing/setup.mdx new file mode 100644 index 0000000000..61ab50732c --- /dev/null +++ b/learn/developing/setup.mdx @@ -0,0 +1,92 @@ +# Setting Up Your Development Environment + +In this guide, we'll walk through setting up your development environment for Telescope. By the end, you'll have everything you need to start coding. + +## What You'll Need + +Before we begin, make sure you have: +- A terminal/command line +- Git installed +- Node.js installed +- Your favorite code editor (we recommend VS Code) + +## Let's Get Started! + +### Step 1: Clone the Repository + +Open your terminal and run: + +```bash +# Clone the telescope repository +git clone https://github.com/hyperweb-io/telescope.git + +# Navigate into the project folder +cd telescope +``` + +### Step 2: Install Dependencies + +Now, let's set up all the packages: + +```bash +# This installs dependencies and links local packages +yarn bootstrap +``` + +This might take a minute or two. The `bootstrap` command not only installs dependencies but also links the local packages instead of using the versions from npm. + +### Step 3: Build the Project + +Let's compile everything to make sure it's working: + +```bash +# This recursively builds all packages +yarn build +``` + +The build process creates all the necessary files for each package, including TypeScript definitions. + +## Troubleshooting Common Issues + +### "Cannot find module" errors + +If you see an error like this, try: + +```bash +# Make sure you're in the root directory +cd /path/to/telescope + +# Try reinstalling and rebuilding +yarn clean +yarn bootstrap +yarn build +``` + +### TypeScript Errors + +If you encounter TypeScript errors: + +```bash +# Rebuild TypeScript definitions +yarn build:ts +``` + +## Next Steps + +Congratulations! Your development environment is now set up. Here's what you can do next: + +1. Explore the code structure in the `packages` directory +2. Try running the tests with `yarn test` +3. Check out the [Packages guide](/learn/developing/packages) to understand how everything fits together + +Remember, if you modify code in one package that's used by another, you'll need to rebuild the package: + +```bash +# Go to the package directory +cd packages/ast + +# Build the package +yarn build +``` + +Happy coding! \ No newline at end of file diff --git a/learn/developing/starship-tests.mdx b/learn/developing/starship-tests.mdx new file mode 100644 index 0000000000..7b4d01be9b --- /dev/null +++ b/learn/developing/starship-tests.mdx @@ -0,0 +1,199 @@ +# Testing with Starship + +In this guide, you'll learn how to use Starship for end-to-end testing of your Telescope features. Starship provides a virtual environment to test generated code against real blockchain nodes. + +## What is Starship? + +Starship is a testing framework that spins up a local Kubernetes cluster with blockchain nodes, allowing you to test your generated code against actual blockchain implementations. + +## Setting Up Your Environment + +### Step 1: Install Dependencies + +First, you'll need to install the necessary tools: + +```bash +# Install Docker if you don't have it +# Visit https://docs.docker.com/get-docker/ + +# Verify Docker is running +docker --version +``` + +### Step 2: Install Cluster Tools + +Now, install the required cluster tools: + +```bash +# Install Kind (Kubernetes in Docker) +# For macOS: brew install kind +# For Linux: curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.11.1/kind-linux-amd64 && chmod +x ./kind && sudo mv ./kind /usr/local/bin/ + +# Install kubectl +# For macOS: brew install kubectl +# For Linux: sudo apt-get update && sudo apt-get install -y kubectl + +# Install Helm +# For macOS: brew install helm +# For Linux: curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + +# Install yq +# For macOS: brew install yq +# For Linux: sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CC86BB64 && sudo add-apt-repository ppa:rmescandon/yq && sudo apt update && sudo apt install yq +``` + +### Step 3: Verify Docker Kubernetes + +In Docker Desktop: +1. Go to Settings +2. Navigate to Kubernetes +3. Check "Enable Kubernetes" +4. Click "Apply & Restart" + +## Running Starship Tests + +### Step 1: Navigate to Starship + +```bash +# Go to the starship package +cd telescope/packages/starship +``` + +### Step 2: Start the Environment + +```bash +# Clean up any previous runs (if needed) +make stop + +# Check for running pods +kubectl get pods +# Wait until you see "No resources found in default namespace." + +# Start the Kubernetes pods +make install + +# Check the pod status +kubectl get pods +# Wait until all pods show status "Running" (this may take a few minutes) +``` + +### Step 3: Forward Ports + +In a new terminal: + +```bash +# Open ports for access +make port-forward +``` + +### Step 4: Generate Code + +```bash +# Generate code for testing +npm run codegen +``` + +The generated code will be in `telescope/packages/starship/src/codegen`. + +### Step 5: Run Tests + +```bash +# Run setup tests first +npx jest __tests__/v1/setup.test.ts + +# Then run specific feature tests +npx jest __tests__/v1/authz.test.ts +``` + +## Writing Your Own Starship Tests + +Let's create a simple test for a bank transfer: + +```typescript +// __tests__/v1/my-custom-test.ts +import { getSigningClient } from '../helpers'; + +describe('My Custom Feature', () => { + it('should transfer tokens successfully', async () => { + // Get a signing client + const client = await getSigningClient(); + + // Define test addresses + const sender = 'cosmos1...' // Your test sender address + const recipient = 'cosmos1...' // Your test recipient address + + // Perform a bank transfer using your generated code + const result = await client.sendTokens( + sender, + recipient, + [{ denom: 'stake', amount: '1000' }], + 'auto' + ); + + // Verify the transaction was successful + expect(result.code).toEqual(0); + }); +}); +``` + +## Cleaning Up + +When you're done testing: + +```bash +# Stop port forwarding +# Press Ctrl+C in the terminal where port-forward is running + +# Stop the infrastructure +npm run e2e:stop + +# Optional: Clean up Kind cluster +npm run e2e:clean +``` + +## Troubleshooting + +### Pods Not Starting + +If pods aren't starting: + +```bash +# Check pod status in detail +kubectl describe pods + +# Check logs for a specific pod +kubectl logs + +# Restart the setup +make stop +make install +``` + +### Network Issues + +If tests fail with network errors: + +```bash +# Check if port forwarding is running +# Restart port forwarding +make port-forward +``` + +### Test Failures + +If tests are failing: + +1. Check the transaction logs +2. Verify that the correct addresses and denoms are being used +3. Ensure your generated code is up to date + +## Next Steps + +Now that you know how to use Starship: + +1. Create more complex test scenarios +2. Test different Telescope generators +3. Contribute to improving the Starship infrastructure +4. Check out the [Creating New Generators](/learn/developing/creating-new-generators) guide to see what else you can build + +Happy testing with Starship! \ No newline at end of file diff --git a/learn/developing/tests.mdx b/learn/developing/tests.mdx new file mode 100644 index 0000000000..5711cd074f --- /dev/null +++ b/learn/developing/tests.mdx @@ -0,0 +1,154 @@ +# Writing and Running Tests + +In this guide, you'll learn how to effectively test your Telescope code. We'll cover how to run existing tests and create new ones. + +## Why Testing Matters + +Testing ensures your code works as expected and prevents regressions when making changes. In Telescope, we primarily use Jest for testing. + +## Running Existing Tests + +Let's start by running tests for different packages: + +### For the AST Package + +```bash +# Navigate to the AST package +cd ./packages/ast + +# Run the tests once +yarn test + +# Run tests in watch mode (great for development) +yarn test:watch +``` + +### For the Parser Package + +```bash +# Navigate to the parser package +cd ./packages/parser + +# Run the tests +yarn test:watch +``` + +### For the Telescope Package + +```bash +# Navigate to the telescope package +cd ./packages/telescope + +# Run the tests +yarn test:watch +``` + +The Telescope package tests will generate fixtures, so you can see the end result of your code. + +## Creating Your Own Tests + +Let's write a simple test for a custom generator: + +### Step 1: Create a Test File + +Create a file named `my-generator.test.ts` in the appropriate package: + +```ts +import { ProtoStore, traverse, getNestedProto } from '@cosmology/proto-parser'; +import { defaultTelescopeOptions } from '@cosmology/types'; +import { expectCode, getTestProtoStore } from '../../../test-utils'; +import { GenericParseContext } from '../../encoding'; +import { createMyGenerator } from './my-generator'; + +describe('My Generator', () => { + it('should generate expected code', () => { + // Set up a test proto store + const store = getTestProtoStore(); + store.options = { ...defaultTelescopeOptions }; + store.traverseAll(); + + // Find a proto to test with + const ref = store.findProto('cosmos/bank/v1beta1/tx.proto'); + const res = traverse(store, ref); + const service = getNestedProto(res).Msg; + + // Create context + const context = new GenericParseContext(ref, store, store.options); + + // Test your generator + expectCode(createMyGenerator(context, service)); + }); +}); +``` + +### Step 2: Run Your Test + +```bash +# Run just your test +yarn test -t "My Generator" + +# Or run all tests in watch mode +yarn test:watch +``` + +## Debugging Tests + +If your tests aren't passing, try these techniques: + +### Viewing Generated Code + +```ts +// Add this to your test to see the generated code +printCode(createMyGenerator(context, service)); +``` + +### Using Console.log + +```ts +// Add debugging inside your generator +console.log('Service:', service); +``` + +### Running a Single Test + +```bash +# Run a specific test file +yarn test my-generator.test.ts +``` + +## Test Best Practices + +1. **Write tests first**: Define what you want your code to do before writing it +2. **Keep tests focused**: Each test should verify one specific behavior +3. **Use meaningful names**: Name your tests clearly to describe what they're checking +4. **Use snapshots wisely**: Snapshots are great for testing generated code + +## Example: Testing a Message Generator + +```ts +describe('MessageGenerator', () => { + it('should add documentation to messages', () => { + const generator = new MessageGenerator(); + const input = { + name: 'User', + comment: '// This is a user message' + }; + + const result = generator.generateWithDocs(input); + + expect(result).toContain('* This is a user message'); + expect(result).toContain('export interface User'); + }); +}); +``` + +## Next Steps + +Now that you know how to test your code: + +1. Run the tests for each package to get familiar with the codebase +2. Try modifying an existing test to see how it works +3. Write a test for your own feature +4. Check out the [Working with ASTs](/learn/developing/working-with-asts) guide to learn more about code generation + +Happy testing! \ No newline at end of file diff --git a/learn/developing/tree-shakable-hooks.mdx b/learn/developing/tree-shakable-hooks.mdx new file mode 100644 index 0000000000..e1365ad513 --- /dev/null +++ b/learn/developing/tree-shakable-hooks.mdx @@ -0,0 +1,332 @@ +# Implementing Tree-Shakable Hooks + +In this tutorial, you'll learn how to use Telescope's tree-shakable hooks feature for both React and Vue applications. We'll walk through the setup, configuration, and implementation with practical examples. + +## What Are Tree-Shakable Hooks? + +Tree-shakable hooks are a new feature in Telescope that replaces the older `reactQuery` option. These hooks are more efficient because they: + +1. Allow you to import only the hooks you need +2. Reduce bundle size through tree-shaking +3. Support both React and Vue frameworks +4. Make your code more modular and maintainable + +## Setting Up Tree-Shakable Hooks + +Let's get started by configuring your Telescope project to generate tree-shakable hooks: + +### Step 1: Update Your Configuration + +Create or update your Telescope configuration file: + +```typescript +// telescope.config.ts +import { TelescopeOptions } from '@cosmology/types'; + +const options: TelescopeOptions = { + // Existing options... + + // Enable helper functions and hooks + helperFunctions: { + enabled: true, + hooks: { + react: true, // Enable React hooks + vue: true // Enable Vue hooks (optional) + } + } +}; + +export default options; +``` + +### Step 2: Run Telescope + +Generate the code with your updated configuration: + +```bash +telescope transpile --config telescope.config.ts +``` + +### Step 3: Explore Generated Files + +After transpile, you'll find these new files: + +``` +output/ +├── cosmos/ +│ ├── react-query/ # React hooks directory +│ ├── vue-query/ # Vue hooks directory +│ └── bank/v1beta1/ +│ ├── query.rpc.func.ts # Shared functions +│ ├── query.rpc.react.ts # React hooks +│ └── query.rpc.vue.ts # Vue hooks +``` + +## Understanding the Generated Files + +Let's look at what each type of file contains: + +### 1. Shared Functions (`query.rpc.func.ts`) + +These are framework-agnostic functions that both React and Vue hooks can use: + +```typescript +// cosmos/bank/v1beta1/query.rpc.func.ts +export const createGetBalance = (clientResolver?: RpcResolver) => + buildQuery({ + encode: QueryBalanceRequest.encode, + decode: QueryBalanceResponse.decode, + service: "cosmos.bank.v1beta1.Query", + method: "Balance", + clientResolver, + deps: [QueryBalanceRequest, QueryBalanceResponse], + }); +``` + +### 2. React Hooks (`query.rpc.react.ts`) + +React-specific hooks built on top of the shared functions: + +```typescript +// cosmos/bank/v1beta1/query.rpc.react.ts +export const useGetBalance = buildUseQuery({ + builderQueryFn: createGetBalance, + queryKeyPrefix: "BalanceQuery", +}); +``` + +### 3. Vue Hooks (`query.rpc.vue.ts`) + +Vue-specific hooks built on top of the shared functions: + +```typescript +// cosmos/bank/v1beta1/query.rpc.vue.ts +export const useGetBalance = buildUseVueQuery({ + builderQueryFn: createGetBalance, + queryKeyPrefix: "BalanceQuery", +}); +``` + +## Using Tree-Shakable Hooks + +Now let's see how to use these hooks in your application: + +### Example 1: Using Shared Functions Directly + +This approach is framework-agnostic: + +```typescript +import { createGetBalance } from "@interchainjs/cosmos-types/cosmos/bank/v1beta1/query.rpc.func"; + +async function getAccountBalance() { + // Get chain info and RPC endpoint + const { getRpcEndpoint } = useChain('osmosis'); + const rpcEndpoint = await getRpcEndpoint(); + + // Create the function with the RPC endpoint + const getBalance = createGetBalance(rpcEndpoint); + + // Execute the query + const { balance } = await getBalance({ + address: "osmo1...", + denom: "uosmo", + }); + + return balance; +} +``` + +### Example 2: Using React Hooks + +In a React component: + +```tsx +import { useGetBalance } from '@interchainjs/react/cosmos/bank/v1beta1/query.rpc.react'; + +function AccountBalance({ address }) { + const { data, isLoading, error } = useGetBalance({ + request: { + address, + denom: "uosmo", + }, + clientResolver: "https://rpc.osmosis.zone", + options: { + enabled: !!address, + } + }); + + if (isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return
Balance: {data?.balance?.amount || '0'} uosmo
; +} +``` + +### Example 3: Using Vue Hooks + +In a Vue component: + +```vue + + + +``` + +## Creating Advanced Hooks + +Let's create a more advanced hook for handling balance with formatting: + +### For React + +```tsx +// hooks/useFormattedBalance.ts +import { useGetBalance } from '@interchainjs/react/cosmos/bank/v1beta1/query.rpc.react'; +import BigNumber from 'bignumber.js'; + +export function useFormattedBalance(address: string, denom: string, exponent: number = 6) { + const { data, isLoading, error, refetch } = useGetBalance({ + request: { + address, + denom, + }, + clientResolver: 'https://rpc.osmosis.zone', + options: { + enabled: !!address, + select: (data) => { + const amount = data.balance?.amount || '0'; + return new BigNumber(amount).multipliedBy(10 ** -exponent).toString(); + }, + } + }); + + return { + balance: data, + isLoading, + error, + refetch, + }; +} + +// Usage in component +function MyComponent() { + const { balance } = useFormattedBalance('osmo1...', 'uosmo', 6); + return
Balance: {balance} OSMO
; +} +``` + +### For Vue + +```typescript +// hooks/useFormattedBalance.ts +import { computed, ref } from 'vue'; +import { useGetBalance } from '@interchainjs/vue/cosmos/bank/v1beta1/query.rpc.vue'; +import BigNumber from 'bignumber.js'; + +export function useFormattedBalance(address, denom, exponent = 6) { + const request = computed(() => ({ + address: address.value, + denom, + })); + + const { data, isLoading, error, refetch } = useGetBalance({ + request, + clientResolver: 'https://rpc.osmosis.zone', + options: { + enabled: !!address.value, + select: (data) => { + const amount = data.balance?.amount || '0'; + return new BigNumber(amount).multipliedBy(10 ** -exponent).toString(); + }, + } + }); + + return { + balance: data, + isLoading, + error, + refetch, + }; +} +``` + +## Best Practices + +When working with tree-shakable hooks: + +1. **Import Selectively**: Only import the hooks you need +2. **Create Custom Hooks**: Wrap the generated hooks with your own business logic +3. **Handle Loading and Errors**: Always handle loading states and errors +4. **Use TypeScript**: Leverage TypeScript for type safety +5. **Memoize Requests**: Use `useMemo` (React) or `computed` (Vue) for request objects + +## Migrating from ReactQuery + +If you're migrating from the old `reactQuery` option: + +1. Update your configuration as shown above +2. Replace imports from `@generated/client` with specific hook imports +3. Update hook usage with the new pattern (passing `request` and `clientResolver`) +4. Add error handling for the new return structure + +## Troubleshooting + +### Hook Not Found + +If you get "module not found" errors: + +```bash +# Check that the files were generated +ls -la output/cosmos/react-query + +# Ensure your import paths are correct +import { useGetBalance } from '@interchainjs/react/cosmos/bank/v1beta1/query.rpc.react'; +``` + +### Type Errors + +If you get TypeScript errors: + +```typescript +// Make sure you're passing the correct types +const request: QueryBalanceRequest = { + address: "osmo1...", + denom: "uosmo", +}; +``` + +## Next Steps + +Now that you've learned about tree-shakable hooks: + +1. Update your Telescope configuration +2. Explore the generated hook files +3. Start using the hooks in your application +4. Create custom hooks for your business logic + +Congratulations! You're now using Telescope's tree-shakable hooks to build more efficient blockchain applications. \ No newline at end of file diff --git a/learn/developing/working-with-asts.mdx b/learn/developing/working-with-asts.mdx new file mode 100644 index 0000000000..23c85040ea --- /dev/null +++ b/learn/developing/working-with-asts.mdx @@ -0,0 +1,244 @@ +# Working with Abstract Syntax Trees (ASTs) + +In this tutorial, you'll learn how to work with Abstract Syntax Trees (ASTs) in Telescope. ASTs are the backbone of Telescope's code generation capabilities. + +## What Are ASTs? + +An Abstract Syntax Tree is a tree representation of the abstract syntactic structure of source code. In Telescope, we use ASTs to transform Protocol Buffer definitions into TypeScript code. + +## Your First AST Experiment + +Let's start with a simple experiment to understand ASTs: + +### Step 1: Navigate to the AST Package + +```bash +cd ./packages/ast +``` + +### Step 2: Create a Test Fixture + +Edit the file `./scripts/fixture.ts` with this content: + +```typescript +// ./scripts/fixture.ts +export interface User { + id: string; + name: string; + age: number; +} +``` + +### Step 3: Generate the AST + +Run the AST generator: + +```bash +yarn test:ast +``` + +### Step 4: Examine the Output + +Open the generated JSON file: + +```bash +code ./scripts/test-output.json +``` + +You'll see something like this (simplified): + +```json +{ + "type": "File", + "program": { + "type": "Program", + "body": [ + { + "type": "ExportNamedDeclaration", + "exportKind": "type", + "declaration": { + "type": "TSInterfaceDeclaration", + "id": { + "type": "Identifier", + "name": "User" + }, + "body": { + "type": "TSInterfaceBody", + "body": [ + { + "type": "TSPropertySignature", + "key": { + "type": "Identifier", + "name": "id" + }, + "typeAnnotation": { + "type": "TSTypeAnnotation", + "typeAnnotation": { + "type": "TSStringKeyword" + } + } + }, + // more properties... + ] + } + } + } + ] + } +} +``` + +## Creating Your Own AST Generator + +Now, let's create a simple AST generator: + +### Step 1: Create a New File + +Create a file `packages/ast/src/my-generator.ts`: + +```typescript +import * as t from '@babel/types'; +import { GenericParseContext } from '../encoding'; + +export const createMyGenerator = ( + context: GenericParseContext, + service: any +) => { + // Add a utility that we need + context.addUtil('SuperInterface'); + + // Create an interface + return t.exportNamedDeclaration( + t.tsInterfaceDeclaration( + t.identifier('MyCustomInterface'), + null, + [t.tsExpressionWithTypeArguments(t.identifier('SuperInterface'))], + t.tsInterfaceBody([ + // Add a property + t.tsPropertySignature( + t.identifier('name'), + t.tsTypeAnnotation(t.tsStringKeyword()) + ), + // Add another property + t.tsPropertySignature( + t.identifier('value'), + t.tsTypeAnnotation(t.tsNumberKeyword()) + ) + ]) + ) + ); +}; +``` + +### Step 2: Create a Test + +Create a file `packages/ast/src/my-generator.test.ts`: + +```typescript +import { ProtoStore, traverse } from '@cosmology/proto-parser'; +import { defaultTelescopeOptions } from '@cosmology/types'; +import { expectCode, getTestProtoStore } from '../test-utils'; +import { GenericParseContext } from '../encoding'; +import { createMyGenerator } from './my-generator'; + +describe('My Generator', () => { + it('should generate expected code', () => { + // Set up a test proto store + const store = getTestProtoStore(); + store.options = { ...defaultTelescopeOptions }; + store.traverseAll(); + + // Find a proto to test with + const ref = store.findProto('cosmos/bank/v1beta1/tx.proto'); + const context = new GenericParseContext(ref, store, store.options); + + // Test your generator with a simple service object + const service = { name: 'TestService' }; + + // For development, print the code to see what it generates + // printCode(createMyGenerator(context, service)); + + // Use expectCode for actual testing, which creates a snapshot + expectCode(createMyGenerator(context, service)); + }); +}); +``` + +### Step 3: Run Your Test + +```bash +cd packages/ast +yarn test -t "My Generator" +``` + +## Common AST Patterns + +Here are some common patterns you'll use when working with ASTs: + +### Creating an Interface + +```typescript +// Create an interface with properties +t.tsInterfaceDeclaration( + t.identifier('User'), + null, + [], + t.tsInterfaceBody([ + t.tsPropertySignature( + t.identifier('id'), + t.tsTypeAnnotation(t.tsStringKeyword()) + ) + ]) +) +``` + +### Creating a Class + +```typescript +// Create a class with methods +t.classDeclaration( + t.identifier('UserService'), + null, + t.classBody([ + t.classMethod( + 'method', + t.identifier('getUser'), + [t.identifier('id')], + t.blockStatement([ + // Method body + ]) + ) + ]) +) +``` + +### Creating Functions + +```typescript +// Create a function declaration +t.functionDeclaration( + t.identifier('processUser'), + [t.identifier('user')], + t.blockStatement([ + // Function body + ]) +) +``` + +## Tips for Working with ASTs + +1. **Use the Test Output**: The `printCode` function helps you see what your AST generates +2. **Start Simple**: Begin with basic structures and build up +3. **Look at Existing Generators**: Study the existing code in `packages/ast/src` +4. **Use AST Explorer**: Visit [astexplorer.net](https://astexplorer.net) to visualize ASTs + +## Next Steps + +Now that you understand ASTs: + +1. Try creating more complex generators +2. Examine how the existing generators work +3. Modify one of the existing generators to add new functionality +4. Check out the [Creating New Generators](/learn/developing/creating-new-generators) guide + +Remember, ASTs are powerful but can be complex. Take your time to understand them, and don't hesitate to experiment! \ No newline at end of file diff --git a/learn/helper-functions-configuration.mdx b/learn/helper-functions-configuration.mdx new file mode 100644 index 0000000000..56ee28c9b0 --- /dev/null +++ b/learn/helper-functions-configuration.mdx @@ -0,0 +1,571 @@ +# Creating Custom Helper Functions + +In this tutorial, we'll explore how to configure Telescope to generate custom helper functions for your blockchain interactions. These helper functions provide a more intuitive and streamlined way to interact with your blockchain's services, simplifying your application code and improving developer experience. + +## Understanding Helper Functions + +When working with Cosmos SDK blockchains, you often need to interact with various services through RPC or LCD clients. This typically involves creating clients, formatting requests, and handling responses. While Telescope generates comprehensive clients for all your proto definitions, sometimes you want a simpler interface focused on specific use cases. + +This is where helper functions come in. They provide: + +1. **Simplified interfaces** - Direct functions that abstract away client creation and management +2. **Intuitive naming** - Custom function names that better reflect their purpose in your application +3. **React/Vue hooks** - Framework-specific hooks for state management (optional) +4. **Focused scope** - Generate helpers only for the services you actually use + +## Setting Up Your Project + +For this tutorial, let's start with a basic Telescope project. If you don't have one set up already, follow these steps: + +```bash +mkdir helper-functions-demo +cd helper-functions-demo +npm init -y +npm install --save-dev @cosmology/telescope +``` + +Next, create a basic Telescope configuration file (telescope.config.js): + +```javascript +const { join } = require('path'); + +module.exports = { + protoDirs: [join(__dirname, './proto')], + outPath: join(__dirname, './src/generated'), + options: { + // We'll add our helper function configuration here + } +}; +``` + +## Basic Helper Function Configuration + +Let's start with a basic configuration to generate helper functions for all services: + +```javascript +module.exports = { + protoDirs: [join(__dirname, './proto')], + outPath: join(__dirname, './src/generated'), + options: { + helperFunctions: { + enabled: true + } + } +}; +``` + +With this minimal configuration, Telescope will generate a `.func.ts` file alongside each proto service file, containing simple helper functions for each method in the service. + +For example, if you have a `cosmos.bank.v1beta1.Query` service with a `balance` method, Telescope will generate: + +```typescript +// In cosmos/bank/v1beta1/query.func.ts +export function getBalance( + rpcEndpoint: string, + params: QueryBalanceRequest +): Promise { + // Implementation that creates a client and calls the service +} +``` + +Notice that Telescope has automatically prefixed the function with `get` since this is a Query service. + +## Customizing Helper Function Names + +While the default naming is often sufficient, you might want to customize the function names to better fit your application's naming conventions. Let's update our configuration: + +```javascript +module.exports = { + // ... + options: { + helperFunctions: { + enabled: true, + nameMappers: { + Query: { + funcBody: (name) => `fetch${name}` // Instead of "get" prefix + }, + Msg: { + funcBody: (name) => `send${name}` // For transaction functions + } + } + } + } +}; +``` + +Now, the generated functions will be: + +```typescript +// For Query services +export function fetchBalance(/* ... */) { /* ... */ } + +// For Msg services +export function sendSend(/* ... */) { /* ... */ } +``` + +You might notice that `sendSend` doesn't sound great. Let's make our naming more specific: + +```javascript +nameMappers: { + Query: { + funcBody: (name) => `fetch${name}` + }, + Msg: { + // Custom transformer function + funcBody: (name) => { + if (name.toLowerCase() === 'send') { + return 'transferTokens'; + } + return `send${name}`; + } + } +} +``` + +Now our bank function will be named `transferTokens` instead of the redundant `sendSend`. + +## Pattern-specific Naming + +You might want different naming conventions for different services. Telescope supports pattern-based naming rules: + +```javascript +nameMappers: { + Query: { + // Default for all Query services + funcBody: (name) => `get${name}`, + + // Special rule for governance proposals + "cosmos.gov.v1beta1.*Proposal*": { + funcBody: (name) => `govQuery${name}` + } + }, + Msg: { + // Default for all Msg services + funcBody: "unchanged", // Use original method name + + // Special rule for staking + "cosmos.staking.v1beta1.*": { + funcBody: (name) => `stake${name.charAt(0).toUpperCase()}${name.slice(1)}` + } + } +} +``` + +With this configuration: + +- `cosmos.gov.v1beta1.Query.proposals` becomes `govQueryProposals` +- `cosmos.staking.v1beta1.Msg.delegate` becomes `stakeDelegate` +- Other Query services use the `get` prefix +- Other Msg services use their original names + +## Generating React Hooks + +If you're using React in your project, you can configure Telescope to generate React Query hooks: + +```javascript +module.exports = { + // ... + options: { + helperFunctions: { + enabled: true, + hooks: { + react: true + }, + nameMappers: { + Query: { + funcBody: (name) => `get${name}`, + hookPrefix: "use" // Default, could be customized + }, + Msg: { + funcBody: "unchanged", + hookPrefix: "useTx" // Custom prefix for transaction hooks + } + } + } + } +}; +``` + +Now, in addition to the regular helper functions, Telescope will generate React hooks: + +```typescript +// Regular function +export function getBalance(/* ... */) { /* ... */ } + +// React Query hook +export function useBalance( + rpcEndpoint: string, + params: QueryBalanceRequest, + options?: UseQueryOptions +): UseQueryResult { + return useQuery( + ['balance', rpcEndpoint, params], + () => getBalance(rpcEndpoint, params), + options + ); +} + +// Transaction hook +export function useTxSend( + rpcEndpoint: string, + options?: UseMutationOptions +): UseMutationResult { + return useMutation( + (params) => send(rpcEndpoint, params), + options + ); +} +``` + +These hooks integrate seamlessly with React Query, providing automatic caching, refetching, and loading states. + +## Generating Vue Composables + +If you're a Vue developer, you can generate Vue Query composables instead: + +```javascript +hooks: { + vue: true, + react: false +} +``` + +This will generate Vue composables using the same naming rules: + +```typescript +// Vue Query composable +export function useBalance( + rpcEndpoint: string, + params: QueryBalanceRequest, + options?: UseQueryOptions +): { + data: Ref; + isLoading: Ref; + error: Ref; + // ... other Vue Query properties +} { + return useQuery( + ['balance', rpcEndpoint, params], + () => getBalance(rpcEndpoint, params), + options + ); +} +``` + +## Limiting Generated Helpers + +For larger projects, you might want to generate helpers only for specific services to keep your codebase clean. Use the `include` option: + +```javascript +helperFunctions: { + enabled: true, + include: { + // Only generate for Query services (not Msg services) + serviceTypes: ["Query"], + + // Only generate for these specific patterns + patterns: [ + "cosmos.bank.v1beta1.**", // All bank query methods + "cosmos.gov.v1beta1.Query.proposal", // Only the proposal query + "cosmos.gov.v1beta1.Query.proposals" // And the proposals query + ] + } + // ... other options +} +``` + +## Advanced Pattern Matching + +The pattern matching in Telescope is quite flexible, supporting glob-style patterns: + +- `**` - Match any number of segments +- `*` - Match any characters within a segment + +For example: + +- `cosmos.bank.**` - All bank services and methods +- `cosmos.*.v1beta1.Query.*Balance*` - Any v1beta1 Query method with "Balance" in its name, across any module +- `**.delegation` - Any method named "delegation" in any service + +## A Complete Example + +Let's put everything together in a comprehensive example: + +```javascript +const { join } = require('path'); + +module.exports = { + protoDirs: [join(__dirname, './proto')], + outPath: join(__dirname, './src/generated'), + options: { + helperFunctions: { + enabled: true, + hooks: { + react: true, + vue: false + }, + include: { + serviceTypes: ["Query", "Msg"], + patterns: [ + "cosmos.bank.v1beta1.**", + "cosmos.staking.v1beta1.**", + "cosmos.gov.v1beta1.**" + ] + }, + nameMappers: { + // Global patterns (applied to both Query and Msg) + All: { + // Special handling for votes + "cosmos.gov.v1beta1.*Vote*": { + funcBody: (name) => `governance${name}`, + hookPrefix: "useGov" + } + }, + + // Query-specific patterns + Query: { + // Default for all Query services + funcBody: (name) => `get${name}`, + hookPrefix: "use", + + // Bank balance queries + "cosmos.bank.v1beta1.*Balance*": { + funcBody: (name) => `fetch${name}`, + hookPrefix: "useBank" + }, + + // Staking queries + "cosmos.staking.v1beta1.*": { + funcBody: (name) => `staking${name}`, + hookPrefix: "useStaking" + } + }, + + // Msg-specific patterns + Msg: { + // Default for all Msg services + funcBody: "unchanged", + hookPrefix: "useTx", + + // Bank transactions + "cosmos.bank.v1beta1.send": { + funcBody: () => "transferTokens", + hookPrefix: "useBankTx" + }, + + // Staking transactions + "cosmos.staking.v1beta1.*": { + funcBody: (name) => `stake${name.charAt(0).toUpperCase()}${name.slice(1)}`, + hookPrefix: "useStakingTx" + } + } + } + } + } +}; +``` + +## Using Generated Helper Functions + +Now that we've configured our helper functions, let's see how to use them in our application: + +### Basic Helper Functions + +```typescript +import { getBalance } from './generated/cosmos/bank/v1beta1/query.func'; + +async function displayBalance(address: string) { + const balance = await getBalance( + 'https://rpc.cosmos.network', + { address, denom: 'uatom' } + ); + + console.log(`Balance: ${parseInt(balance.balance.amount) / 1_000_000} ATOM`); +} +``` + +### React Hooks + +```tsx +import React from 'react'; +import { useBalance } from './generated/cosmos/bank/v1beta1/query.func'; + +function BalanceDisplay({ address }: { address: string }) { + const { data, isLoading, error } = useBalance( + 'https://rpc.cosmos.network', + { address, denom: 'uatom' } + ); + + if (isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; + + const atomAmount = parseInt(data?.balance?.amount || '0') / 1_000_000; + + return ( +
+

Account Balance

+

{atomAmount} ATOM

+
+ ); +} +``` + +### Transaction Hooks + +```tsx +import React from 'react'; +import { useBankTxTransferTokens } from './generated/cosmos/bank/v1beta1/tx.func'; + +function SendTokensForm() { + const [recipient, setRecipient] = React.useState(''); + const [amount, setAmount] = React.useState(''); + + const mutation = useBankTxTransferTokens('https://rpc.cosmos.network'); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const atomAmount = parseFloat(amount) * 1_000_000; // Convert to uatom + + await mutation.mutateAsync({ + fromAddress: 'cosmos1yoursenderaddress', + toAddress: recipient, + amount: [{ denom: 'uatom', amount: atomAmount.toString() }] + }); + }; + + return ( +
+

Send Tokens

+
+ + setRecipient(e.target.value)} + /> +
+
+ + setAmount(e.target.value)} + /> +
+ + + {mutation.isError && ( +
Error: {mutation.error?.message}
+ )} + + {mutation.isSuccess && ( +
Success! Transaction hash: {mutation.data?.transactionHash}
+ )} +
+ ); +} +``` + +## Best Practices for Helper Functions + +To get the most out of Telescope's helper function generation, follow these best practices: + +### 1. Use Consistent Naming Conventions + +Establish clear naming conventions for different types of operations: + +- **Queries**: Use prefixes like `get`, `fetch`, or `query` +- **Transactions**: Use action verbs like `send`, `execute`, or `perform` +- **Hooks**: Use standard prefixes like `use` for React and Vue + +### 2. Limit Generated Helpers to What You Need + +Only generate helpers for the services you actually use in your application: + +```javascript +include: { + patterns: [ + // Only the modules and methods you actually use + "cosmos.bank.v1beta1.Query.balance", + "cosmos.bank.v1beta1.Query.allBalances", + "cosmos.bank.v1beta1.Msg.send" + ] +} +``` + +### 3. Create Domain-Specific Helper Groups + +Group related functionality with consistent naming: + +```javascript +nameMappers: { + Query: { + // Prefixes based on domain + "cosmos.bank.**": { funcBody: (name) => `bank${name}` }, + "cosmos.staking.**": { funcBody: (name) => `staking${name}` }, + "cosmos.gov.**": { funcBody: (name) => `governance${name}` } + } +} +``` + +### 4. Document Your Naming Logic + +If you use complex naming transformations, document them for your team: + +```javascript +// Custom transformer function with documentation +funcBody: (name) => { + // For delegation-related methods, use active verb form + if (name.includes('delegation')) { + return `stake${name.replace('delegation', '')}`; + } + // For validator methods, use 'validator' prefix + if (name.includes('validator')) { + return `validator${name.replace('validator', '')}`; + } + // Default fallback + return `get${name}`; +} +``` + +## Troubleshooting + +### Helper Functions Not Being Generated + +If your helper functions aren't being generated, check: + +1. Is `helperFunctions.enabled` set to `true`? +2. Do your `include.patterns` match the services you expect? +3. Are your proto files being properly processed by Telescope? + +Try running Telescope with debug logging enabled: + +```javascript +options: { + debug: { + enabled: true, + helperFunctionsLog: true + }, + helperFunctions: { + // Your config... + } +} +``` + +### Incorrect Function Names + +If your generated function names don't match your expectations, check: + +1. Are your pattern matches correct? +2. Remember that more specific patterns take precedence +3. Service-specific patterns (`Query`, `Msg`) override `All` patterns + +## Conclusion + +Telescope's helper function generation is a powerful feature that can significantly improve your development experience when working with Cosmos SDK blockchains. By providing a simplified interface to your blockchain's services, you can write cleaner, more intuitive code and focus on your application's unique features rather than boilerplate client code. + +By carefully configuring your helper functions with meaningful names, targeted scoping, and framework integration, you can create a developer-friendly API tailored to your specific needs. + +In the next tutorial, we'll explore other advanced features of Telescope to further enhance your Cosmos SDK development workflow. \ No newline at end of file diff --git a/learn/instant-rpc-methods.mdx b/learn/instant-rpc-methods.mdx new file mode 100644 index 0000000000..9b1069e517 --- /dev/null +++ b/learn/instant-rpc-methods.mdx @@ -0,0 +1,542 @@ +# Using Instant RPC Methods + +In this tutorial, we'll explore how to use Instant RPC Methods with Telescope-generated code to create simplified interfaces for common blockchain operations. This powerful feature allows you to build customized, focused clients that make your code more readable and maintainable. + +## Understanding Instant RPC Methods + +When working with Cosmos SDK blockchains, you often need to interact with many different modules and methods. Standard RPC clients provide access to all these methods through a nested hierarchy, which can lead to verbose code: + +```typescript +// Standard RPC client usage - verbose and deeply nested +const balance = await client.cosmos.bank.v1beta1.balance({ + address: "cosmos1...", + denom: "uatom" +}); +``` + +Instant RPC Methods solve this problem by allowing you to create customized client classes that expose only the specific methods you need, with names you choose: + +```typescript +// Instant RPC client usage - direct and intuitive +const balance = await cosmosClient.getBalance({ + address: "cosmos1...", + denom: "uatom" +}); +``` + +This approach makes your code more readable and focused on your application's needs. + +## Setting Up Your Project + +First, let's enable Instant RPC Methods generation in your Telescope configuration: + +```typescript +// In your Telescope configuration file +import { TelescopeOptions } from "@cosmology/types"; + +const options: TelescopeOptions = { + rpcClients: { + enabled: true, + instantOps: [ + { + className: "CosmosClient", + nameMapping: { + All: { + "cosmos.bank.v1beta1.balance": "getBalance", + "cosmos.bank.v1beta1.allBalances": "getAllBalances", + "cosmos.staking.v1beta1.validators": "getValidators" + } + } + } + ] + } + // Other Telescope options... +}; + +export default options; +``` + +After running Telescope with this configuration, it will generate a `service-ops.ts` file in your project's root directory containing your customized client class. + +## Creating Your First Instant RPC Client + +Let's walk through a simple example of using an Instant RPC client to query account balances: + +```typescript +import { CosmosClient } from "./service-ops"; +import { createRPCQueryClient } from "./codegen/rpc"; + +async function main() { + try { + // First create a regular RPC client + console.log("Connecting to Cosmos Hub..."); + const rpcClient = await createRPCQueryClient({ + rpcEndpoint: "https://rpc.cosmos.network" + }); + + // Initialize our instant client + const cosmosClient = new CosmosClient(); + cosmosClient.init(rpcClient); + + console.log("Client created successfully!"); + + // Query an account's balance using our simplified method + const address = "cosmos1..."; // Replace with a real address + const { balance } = await cosmosClient.getBalance({ + address, + denom: "uatom" + }); + + if (balance) { + const atomAmount = parseInt(balance.amount) / 1_000_000; + console.log(`Account has ${atomAmount} ATOM`); + } else { + console.log("Account has no ATOM balance"); + } + + // Query all balances for the account + const { balances } = await cosmosClient.getAllBalances({ + address + }); + + console.log("\nAll account balances:"); + if (balances.length === 0) { + console.log("No tokens found"); + } else { + balances.forEach(coin => { + const amount = coin.denom.startsWith('u') + ? parseInt(coin.amount) / 1_000_000 + : coin.amount; + const symbol = coin.denom.startsWith('u') + ? coin.denom.substring(1).toUpperCase() + : coin.denom; + console.log(`${amount} ${symbol}`); + }); + } + + // Query validators + const { validators } = await cosmosClient.getValidators({ + status: "BOND_STATUS_BONDED" + }); + + console.log(`\nFound ${validators.length} active validators`); + console.log("Top 3 validators:"); + validators + .sort((a, b) => parseInt(b.tokens) - parseInt(a.tokens)) + .slice(0, 3) + .forEach((validator, index) => { + console.log(`${index + 1}. ${validator.description.moniker}`); + }); + } catch (error) { + console.error("Error:", error.message); + } +} + +main(); +``` + +## Customizing Method Names + +One of the powerful features of Instant RPC Methods is the ability to customize method names. Let's see how different naming mappings affect your client's interface: + +```typescript +// In your configuration +nameMapping: { + All: { + // Original format: "package.method" -> "customName" + "cosmos.bank.v1beta1.balance": "getAccountBalance", + "cosmos.bank.v1beta1.allBalances": "getAllTokenBalances", + "cosmos.staking.v1beta1.validators": "listValidators" + } +} + +// In your code +const { balance } = await cosmosClient.getAccountBalance({ + address: "cosmos1...", + denom: "uatom" +}); + +const { validators } = await cosmosClient.listValidators({}); +``` + +Choose names that make sense for your application's domain, making the code more self-explanatory. + +## Creating Domain-Specific Clients + +For larger applications, you might want to create multiple client classes, each focused on a specific domain. This helps organize your code and makes it more maintainable: + +```typescript +// In your configuration +instantOps: [ + { + className: "BankingClient", + nameMapping: { + All: { + "cosmos.bank.v1beta1.balance": "getBalance", + "cosmos.bank.v1beta1.allBalances": "getAllBalances", + "cosmos.bank.v1beta1.totalSupply": "getTotalSupply" + } + } + }, + { + className: "StakingClient", + nameMapping: { + All: { + "cosmos.staking.v1beta1.validators": "getValidators", + "cosmos.staking.v1beta1.delegatorDelegations": "getDelegations", + "cosmos.staking.v1beta1.delegatorUnbondingDelegations": "getUnbonding" + } + } + }, + { + className: "GovernanceClient", + nameMapping: { + All: { + "cosmos.gov.v1beta1.proposals": "getProposals", + "cosmos.gov.v1beta1.proposal": "getProposal", + "cosmos.gov.v1beta1.params": "getGovParams" + } + } + } +] + +// In your code +import { BankingClient, StakingClient, GovernanceClient } from "./service-ops"; + +// Initialize clients +const bankingClient = new BankingClient(); +const stakingClient = new StakingClient(); +const govClient = new GovernanceClient(); + +// Connect all clients to the same RPC endpoint +const rpcClient = await createRPCQueryClient({ + rpcEndpoint: "https://rpc.cosmos.network" +}); + +bankingClient.init(rpcClient); +stakingClient.init(rpcClient); +govClient.init(rpcClient); + +// Use domain-specific clients +const { balances } = await bankingClient.getAllBalances({ address }); +const { validators } = await stakingClient.getValidators({}); +const { proposals } = await govClient.getProposals({}); +``` + +## Combining Query and Transaction Methods + +Instant RPC clients can also combine query methods (for reading data) and transaction methods (for writing data) in a single client. To do this, use the `Query` and `Msg` categories in your name mapping: + +```typescript +// In your configuration +nameMapping: { + Query: { + "cosmos.bank.v1beta1.balance": "getBalance", + "cosmos.bank.v1beta1.allBalances": "getAllBalances" + }, + Msg: { + "cosmos.bank.v1beta1.send": "sendTokens", + "cosmos.staking.v1beta1.delegate": "delegateTokens" + } +} + +// In your code - for queries (read operations) +const { balance } = await client.getBalance({ + address: "cosmos1...", + denom: "uatom" +}); + +// In your code - for transactions (write operations) +const sendResult = await client.sendTokens({ + fromAddress: "cosmos1sender...", + toAddress: "cosmos1recipient...", + amount: [{ denom: "uatom", amount: "1000000" }] +}); +``` + +## Building a Portfolio App with Instant RPC Methods + +Let's create a more complete example - a portfolio application that shows a user's balances, staking positions, and rewards: + +```typescript +// Configuration in Telescope options +instantOps: [ + { + className: "PortfolioClient", + nameMapping: { + All: { + // Banking methods + "cosmos.bank.v1beta1.allBalances": "getWalletBalances", + + // Staking methods + "cosmos.staking.v1beta1.delegatorDelegations": "getStakedPositions", + "cosmos.staking.v1beta1.delegatorUnbondingDelegations": "getUnbondingPositions", + + // Distribution methods + "cosmos.distribution.v1beta1.delegationTotalRewards": "getStakingRewards", + + // Validator info + "cosmos.staking.v1beta1.validators": "getValidatorList" + } + } + } +] + +// Application code +import { PortfolioClient } from "./service-ops"; +import { createRPCQueryClient } from "./codegen/rpc"; + +async function displayPortfolio(address) { + try { + // Create and initialize the client + const rpcClient = await createRPCQueryClient({ + rpcEndpoint: "https://rpc.cosmos.network" + }); + + const portfolioClient = new PortfolioClient(); + portfolioClient.init(rpcClient); + + // Fetch all portfolio data in parallel for efficiency + const [ + walletResponse, + stakingResponse, + unbondingResponse, + rewardsResponse + ] = await Promise.all([ + portfolioClient.getWalletBalances({ address }), + portfolioClient.getStakedPositions({ delegatorAddr: address }), + portfolioClient.getUnbondingPositions({ delegatorAddr: address }), + portfolioClient.getStakingRewards({ delegatorAddress: address }) + ]); + + // Get validator details for display + const validatorAddresses = stakingResponse.delegationResponses.map( + del => del.delegation.validatorAddress + ); + + const validatorsResponse = await portfolioClient.getValidatorList({ + // Get details for validators the user has delegated to + validatorAddresses + }); + + // Create a map of validator addresses to names + const validatorMap = {}; + validatorsResponse.validators.forEach(validator => { + validatorMap[validator.operatorAddress] = validator.description.moniker; + }); + + // Process wallet balances + const liquidBalances = walletResponse.balances.map(coin => { + return { + denom: coin.denom, + amount: coin.amount, + displayAmount: formatAmount(coin.amount, coin.denom) + }; + }); + + // Process staked positions + const stakedPositions = stakingResponse.delegationResponses.map(del => { + return { + validatorAddress: del.delegation.validatorAddress, + validatorName: validatorMap[del.delegation.validatorAddress] || "Unknown validator", + amount: del.balance.amount, + denom: del.balance.denom, + displayAmount: formatAmount(del.balance.amount, del.balance.denom) + }; + }); + + // Process unbonding positions + const unbondingPositions = unbondingResponse.unbondingResponses.map(unbond => { + // Calculate total unbonding amount + const totalAmount = unbond.entries.reduce( + (sum, entry) => sum + BigInt(entry.balance), + BigInt(0) + ).toString(); + + return { + validatorAddress: unbond.validatorAddress, + validatorName: validatorMap[unbond.validatorAddress] || "Unknown validator", + entries: unbond.entries.length, + completionTime: new Date(unbond.entries[0].completionTime).toLocaleString(), + totalAmount, + displayAmount: formatAmount(totalAmount, "uatom") + }; + }); + + // Process rewards + const rewards = rewardsResponse.rewards.map(reward => { + return { + validatorAddress: reward.validatorAddress, + validatorName: validatorMap[reward.validatorAddress] || "Unknown validator", + rewards: reward.reward.map(r => ({ + denom: r.denom, + amount: r.amount, + displayAmount: formatAmount(r.amount, r.denom) + })) + }; + }); + + // Calculate total rewards + const totalRewards = rewardsResponse.total.map(r => ({ + denom: r.denom, + amount: r.amount, + displayAmount: formatAmount(r.amount, r.denom) + })); + + // Display the portfolio + console.log(`=== Portfolio for ${address} ===`); + + // Liquid balances + console.log("\nLiquid Balances:"); + if (liquidBalances.length === 0) { + console.log(" No liquid tokens found"); + } else { + liquidBalances.forEach(b => { + console.log(` ${b.displayAmount}`); + }); + } + + // Staked positions + console.log("\nStaked Positions:"); + if (stakedPositions.length === 0) { + console.log(" No staked tokens found"); + } else { + stakedPositions.forEach(p => { + console.log(` ${p.displayAmount} with ${p.validatorName}`); + }); + } + + // Unbonding positions + console.log("\nUnbonding Positions:"); + if (unbondingPositions.length === 0) { + console.log(" No unbonding tokens found"); + } else { + unbondingPositions.forEach(p => { + console.log(` ${p.displayAmount} from ${p.validatorName}`); + console.log(` Complete on: ${p.completionTime}`); + }); + } + + // Rewards + console.log("\nPending Rewards:"); + if (totalRewards.length === 0) { + console.log(" No pending rewards found"); + } else { + totalRewards.forEach(r => { + console.log(` ${r.displayAmount}`); + }); + } + } catch (error) { + console.error("Error displaying portfolio:", error.message); + } +} + +// Helper function to format token amounts for display +function formatAmount(amount, denom) { + if (denom === "uatom") { + const atoms = Number(amount) / 1_000_000; + return `${atoms.toLocaleString()} ATOM`; + } else if (denom.startsWith("u")) { + const standardAmount = Number(amount) / 1_000_000; + const symbol = denom.substring(1).toUpperCase(); + return `${standardAmount.toLocaleString()} ${symbol}`; + } else { + return `${amount} ${denom}`; + } +} + +// Call the function with a real address +displayPortfolio("cosmos1..."); +``` + +## Best Practices and Tips + +Here are some best practices to follow when using Instant RPC Methods: + +### 1. Choose Descriptive Method Names + +Select method names that clearly describe what the method does, making your code more self-explanatory: + +```typescript +// Good naming +"cosmos.bank.v1beta1.balance": "getAccountBalance" +"cosmos.staking.v1beta1.delegatorDelegations": "getStakedPositions" + +// Less clear naming +"cosmos.bank.v1beta1.balance": "getB" +"cosmos.staking.v1beta1.delegatorDelegations": "getDels" +``` + +### 2. Group Related Methods in Domain-Specific Clients + +Create separate clients for different functional areas of your application: + +```typescript +// Banking client +className: "BankClient" + +// Staking client +className: "StakingClient" + +// Governance client +className: "GovClient" +``` + +### 3. Use Consistent Naming Patterns + +Maintain consistency in your method naming to make your API more predictable: + +```typescript +// Consistent pattern for getter methods +"cosmos.bank.v1beta1.balance": "getBalance" +"cosmos.bank.v1beta1.allBalances": "getAllBalances" +"cosmos.staking.v1beta1.validators": "getValidators" + +// Consistent pattern for action methods +"cosmos.bank.v1beta1.send": "sendTokens" +"cosmos.staking.v1beta1.delegate": "delegateTokens" +``` + +### 4. Document Your Custom Methods + +Add comments to explain what your custom methods do, especially if you've significantly altered the original method name: + +```typescript +// In your code +/** + * Gets the current liquid balance of an account for a specific token + * @param address The account address to query + * @param denom The token denomination (e.g., 'uatom') + * @returns The token balance + */ +const { balance } = await client.getBalance({ address, denom }); +``` + +### 5. Initialize Once, Reuse Often + +Initialize your client once and reuse it for multiple operations to avoid unnecessary connection overhead: + +```typescript +// Initialize once +const client = new PortfolioClient(); +client.init(rpcClient); + +// Reuse for multiple operations +const balances = await client.getWalletBalances({ address }); +const staking = await client.getStakedPositions({ delegatorAddr: address }); +const rewards = await client.getStakingRewards({ delegatorAddress: address }); +``` + +## Conclusion + +In this tutorial, you've learned how to use Instant RPC Methods to create customized, focused clients for working with Cosmos SDK blockchains. By using this approach, you can: + +1. Simplify your code by eliminating deep nesting of method calls +2. Create more intuitive interfaces with custom method names +3. Organize your code into domain-specific clients +4. Combine both query and transaction methods in a single client +5. Improve the overall readability and maintainability of your application + +Instant RPC Methods provide a powerful way to tailor the generated Telescope code to your application's specific needs, making it more accessible to other developers on your team and easier to maintain over time. + +In the next tutorial, we'll explore [Manually Registering Types](./manually-registering-types.md) to extend Telescope's capabilities with custom or third-party types. \ No newline at end of file diff --git a/learn/json-patch-protos.mdx b/learn/json-patch-protos.mdx new file mode 100644 index 0000000000..3bd12fa911 --- /dev/null +++ b/learn/json-patch-protos.mdx @@ -0,0 +1,345 @@ +# Modifying Protos with JSON Patch + +In this tutorial, we'll explore how to use Telescope's JSON Patch Protos feature to modify protocol buffer definitions without changing the original files. This powerful capability allows you to customize generated code while keeping your proto files untouched. + +## Understanding the Problem + +When working with Cosmos SDK blockchains, you'll often encounter situations where: + +1. You need to customize the generated TypeScript code +2. The upstream proto files can't be modified because they're maintained by others +3. You're waiting for a pull request to be merged +4. You need to work around issues in the proto definitions + +Instead of forking and maintaining your own version of the proto files, Telescope provides a solution: JSON Patch Protos. + +## What is JSON Patch? + +JSON Patch is a format for describing changes to a JSON document. It consists of operations like "add", "replace", and "remove" that can transform one JSON document into another. Telescope uses this approach to modify proto definitions during code generation. + +## Setting Up Your Project + +First, let's set up a project with Telescope. If you haven't already done so, create a new Telescope project: + +```bash +# Install telescope globally if needed +npm install -g @cosmology/telescope + +# Create a new project +mkdir my-patched-protos +cd my-patched-protos +npm init -y +npm install --save-dev @cosmology/telescope +``` + +Next, create a basic Telescope configuration file (telescope.config.js): + +```javascript +const { join } = require('path'); + +module.exports = { + protoDirs: [join(__dirname, './proto')], + outPath: join(__dirname, './src/generated'), + options: { + // We'll add our patches here + } +}; +``` + +## Your First Patch: Customizing an Enum + +Let's say you're working with the CosmWasm module, and you want to customize the `AccessType` enum to have more descriptive names for better developer experience. + +First, examine the original proto definition (which you can't modify directly): + +```protobuf +// File: cosmwasm/wasm/v1/types.proto +enum AccessType { + // ACCESS_TYPE_UNSPECIFIED placeholder for empty value + ACCESS_TYPE_UNSPECIFIED = 0 [(gogoproto.enumvalue_customname) = "Unspecified"]; + // ACCESS_TYPE_NOBODY nobody can execute this contract + ACCESS_TYPE_NOBODY = 1 [(gogoproto.enumvalue_customname) = "Nobody"]; + // ACCESS_TYPE_ONLY_ADDRESS only a specific address can execute this contract + ACCESS_TYPE_ONLY_ADDRESS = 2 [(gogoproto.enumvalue_customname) = "OnlyAddress"]; + // ACCESS_TYPE_EVERYBODY everybody can execute this contract + ACCESS_TYPE_EVERYBODY = 3 [(gogoproto.enumvalue_customname) = "Everybody"]; +} +``` + +Now, let's modify this enum using JSON Patch by updating our Telescope configuration: + +```javascript +module.exports = { + protoDirs: [join(__dirname, './proto')], + outPath: join(__dirname, './src/generated'), + options: { + prototypes: { + patch: { + "cosmwasm/wasm/v1/types.proto": [ + { + // Replace the custom name for the UNSPECIFIED value + op: "replace", + path: "@/AccessType/valuesOptions/ACCESS_TYPE_UNSPECIFIED/(gogoproto.enumvalue_customname)", + value: "UnspecifiedAccess" + }, + { + // Replace the custom name for the NOBODY value + op: "replace", + path: "@/AccessType/valuesOptions/ACCESS_TYPE_NOBODY/(gogoproto.enumvalue_customname)", + value: "NobodyAccess" + }, + { + // Add a completely new enum value + op: "add", + path: "@/AccessType/values/ACCESS_TYPE_SUPER_ADMIN", + value: 4 + }, + { + // Add custom name for the new enum value + op: "add", + path: "@/AccessType/valuesOptions/ACCESS_TYPE_SUPER_ADMIN", + value: { + "(gogoproto.enumvalue_customname)": "SuperAdminAccess" + } + } + ] + } + } + } +}; +``` + +In this configuration: + +1. We're replacing the custom names for existing enum values +2. We're adding a completely new enum value with ID 4 +3. We're adding a custom name for our new enum value + +After running Telescope code generation, the TypeScript code will include our customized enum with more descriptive names and the additional SuperAdminAccess type. + +## Understanding Path Syntax + +The path in each patch operation points to the target location in the proto definition. Telescope supports two path formats: + +1. **Absolute paths**: Direct JSON paths to the field +2. **Package-derived paths**: Paths prefixed with `@` that are derived from the package name + +For example, in the path `@/AccessType/valuesOptions/ACCESS_TYPE_UNSPECIFIED/(gogoproto.enumvalue_customname)`: + +- `@` indicates this is a package-derived path +- `/AccessType` targets the AccessType enum +- `/valuesOptions/ACCESS_TYPE_UNSPECIFIED` targets the options for the UNSPECIFIED value +- `/(gogoproto.enumvalue_customname)` targets the specific option to replace + +## Adding Fields to Messages + +Beyond modifying enums, you can also add or modify fields in message types. Let's see how to add a new field to a message: + +```javascript +"cosmwasm/wasm/v1/types.proto": [ + { + // Add a new field to the Contract message + op: "add", + path: "@/Contract/fields/admin", + value: { + rule: "optional", + type: "string", + name: "admin", + id: 3, // Choose an unused field number + options: { + "(gogoproto.moretags)": "yaml:\"admin\"" + } + } + } +] +``` + +This adds a new `admin` field to the `Contract` message, which will be included in the generated TypeScript code. + +## Real-World Example: Fixing a Proto Definition + +Let's look at a more practical example. Imagine you're working with a chain that has a proto definition with an issue - a field that should be an array but isn't defined correctly: + +Original problematic proto: +```protobuf +message Validator { + string address = 1; + int64 voting_power = 2; + string public_key = 3; // Should be bytes, not string +} +``` + +You could fix this with a patch: + +```javascript +"cosmos/base/tendermint/v1beta1/query.proto": [ + { + // Change the field type from string to bytes + op: "replace", + path: "@/Validator/fields/public_key/type", + value: "bytes" + }, + { + // Add additional options for proper handling + op: "add", + path: "@/Validator/fields/public_key/options", + value: { + "(gogoproto.casttype)": "github.com/tendermint/tendermint/crypto/crypto.PubKey" + } + } +] +``` + +## Creating Complex Patches + +For more complex modifications, you can combine multiple operations. Let's say you want to modify a message, its fields, and add a new enum all at once: + +```javascript +"mychain/custom/v1/types.proto": [ + // Add a new message + { + op: "add", + path: "@/messages/CustomSettings", + value: { + name: "CustomSettings", + fields: {}, + reserved: [], + extensions: [] + } + }, + + // Add fields to the new message + { + op: "add", + path: "@/CustomSettings/fields/enabled", + value: { + rule: "optional", + type: "bool", + name: "enabled", + id: 1 + } + }, + { + op: "add", + path: "@/CustomSettings/fields/mode", + value: { + rule: "optional", + type: "SettingsMode", + name: "mode", + id: 2 + } + }, + + // Add a new enum for the message + { + op: "add", + path: "@/enums/SettingsMode", + value: { + name: "SettingsMode", + values: { + "SETTINGS_MODE_UNSPECIFIED": 0, + "SETTINGS_MODE_BASIC": 1, + "SETTINGS_MODE_ADVANCED": 2 + }, + valuesOptions: { + "SETTINGS_MODE_UNSPECIFIED": { + "(gogoproto.enumvalue_customname)": "Unspecified" + }, + "SETTINGS_MODE_BASIC": { + "(gogoproto.enumvalue_customname)": "Basic" + }, + "SETTINGS_MODE_ADVANCED": { + "(gogoproto.enumvalue_customname)": "Advanced" + } + } + } + } +] +``` + +## Best Practices + +When using JSON Patch Protos, follow these best practices: + +### 1. Document Your Patches + +Always add comments explaining why each patch exists: + +```javascript +"cosmwasm/wasm/v1/types.proto": [ + { + // PATCH REASON: Better naming for UI display purposes + op: "replace", + path: "@/AccessType/valuesOptions/ACCESS_TYPE_UNSPECIFIED/(gogoproto.enumvalue_customname)", + value: "UnspecifiedAccess" + } +] +``` + +### 2. Keep Patches Minimal + +Only modify what's necessary. Extensive changes make your code harder to maintain when the upstream protos change. + +### 3. Review Regularly + +Revisit your patches after upgrading dependencies to see if they're still needed. + +### 4. Test Thoroughly + +After applying patches, test the generated code to ensure it works as expected: + +```typescript +// Test that your patched enum has the expected values +import { AccessType } from './generated/cosmwasm/wasm/v1/types'; + +console.log(AccessType.UnspecifiedAccess); // Should be 0 +console.log(AccessType.NobodyAccess); // Should be 1 +console.log(AccessType.SuperAdminAccess); // Should be 4 +``` + +### 5. Consider Upstreaming Changes + +If your patches solve a common problem, consider submitting a pull request to the original repository. + +## Limitations to Be Aware Of + +While JSON Patch Protos is powerful, it has some limitations: + +1. **Complex Nested Structures**: Some deeply nested or complex proto structures may be difficult to modify. + +2. **Proto Validation**: Telescope doesn't validate that your patched proto definitions conform to the protobuf specification. + +3. **Changes to Original Structure**: If the original proto files change significantly, your patches may break and need to be updated. + +## Troubleshooting + +If your patches aren't working as expected, try these troubleshooting steps: + +1. **Verify Proto Structure**: Use Telescope's debugging features to output the parsed proto structure and check that your paths match: + +```javascript +options: { + debug: { + printProtoAST: true // Add this to see the full proto structure + }, + prototypes: { + patch: { /* your patches */ } + } +} +``` + +2. **Start Simple**: If you're having trouble with complex patches, start with a simple patch and gradually add complexity. + +3. **Check Path Syntax**: Ensure your paths correctly target the fields you want to modify. + +## Conclusion + +JSON Patch Protos is a powerful feature that allows you to modify proto definitions without changing the original files. This can be particularly useful when: + +- You're waiting for upstream changes to be merged +- You need to adapt proto definitions to your specific needs +- You want to add custom fields or modify behavior without forking + +By using patches judiciously and following best practices, you can customize your generated code while maintaining compatibility with the original proto files. + +In the next tutorial, we'll explore [CosmWasm Integration](./cosmwasm.md) to see how Telescope can generate TypeScript SDKs for your CosmWasm smart contracts. \ No newline at end of file diff --git a/learn/lcd-clients-classes.mdx b/learn/lcd-clients-classes.mdx new file mode 100644 index 0000000000..32776fe184 --- /dev/null +++ b/learn/lcd-clients-classes.mdx @@ -0,0 +1,708 @@ +# Working with LCD Client Classes + +In this tutorial, we'll explore how to work with LCD Client Classes generated by Telescope. These classes provide an object-oriented approach to querying blockchain data, making your code more maintainable and extensible. + +## Understanding LCD Client Classes + +LCD Client Classes take the functionality of LCD Clients (covered in the [previous tutorial](./lcd-clients.md)) and wrap them in a class-based architecture. This approach has several advantages: + +1. **Extensibility**: You can easily extend the classes to add custom methods +2. **Modularity**: Each module is represented by its own class +3. **Testability**: Classes are easier to mock and test +4. **Encapsulation**: Implementation details are hidden behind a clean interface + +Let's dive in and see how to use these classes in your projects. + +## Setting Up LCD Client Classes + +First, you need to enable LCD Client Classes generation in your Telescope configuration: + +```typescript +// In your Telescope configuration +import { TelescopeOptions } from "@cosmology/types"; + +const options: TelescopeOptions = { + lcdClientClasses: { + enabled: true + } + // Other options... +}; + +export default options; +``` + +After running Telescope with this configuration, it will generate client classes for all modules in your Protobuf definitions. + +## Creating Your First Client Class + +Let's start by creating a basic client class to query the Cosmos Hub: + +```typescript +import { createLCDClientClasses } from "./codegen/client-classes"; + +async function main() { + // Create the client classes + const clients = createLCDClientClasses({ + restEndpoint: "https://rest.cosmos.network" + }); + + console.log("Client classes created successfully!"); + + // Access a specific module client + const bankClient = clients.cosmos.bank.v1beta1; + + // Query an account's balance + const address = "cosmos1..."; // Replace with a real address + const { balances } = await bankClient.allBalances({ address }); + + console.log(`Account ${address} has:`); + balances.forEach(coin => { + console.log(`- ${coin.amount} ${coin.denom}`); + }); +} + +main().catch(console.error); +``` + +## Exploring Module Structure + +Let's look closer at how these client classes are structured: + +```typescript +// The client classes follow a nested structure similar to the protobuf packages +const clients = createLCDClientClasses({ + restEndpoint: "https://rest.cosmos.network" +}); + +// Each module has its own client class +const bankClient = clients.cosmos.bank.v1beta1; +const stakingClient = clients.cosmos.staking.v1beta1; +const govClient = clients.cosmos.gov.v1beta1; + +// These classes have methods corresponding to the REST endpoints +async function exploreModules() { + // Bank module methods + console.log("Bank module methods:"); + console.log(Object.getOwnPropertyNames( + Object.getPrototypeOf(bankClient) + ).filter(name => name !== 'constructor')); + + // Staking module methods + console.log("\nStaking module methods:"); + console.log(Object.getOwnPropertyNames( + Object.getPrototypeOf(stakingClient) + ).filter(name => name !== 'constructor')); + + // Gov module methods + console.log("\nGov module methods:"); + console.log(Object.getOwnPropertyNames( + Object.getPrototypeOf(govClient) + ).filter(name => name !== 'constructor')); +} +``` + +## Extending Client Classes + +One of the main advantages of using client classes is the ability to extend them with custom methods. Let's create an extended bank client with helpful utility methods: + +```typescript +import { BankQueryClient } from "./codegen/cosmos/bank/v1beta1/query.lcd"; +import axios from "axios"; + +// Create a specialized bank client with extended functionality +class EnhancedBankClient extends BankQueryClient { + // Constructor stays the same + constructor(axios, baseUrl) { + super(axios, baseUrl); + } + + // Add a method to get formatted token balance + async getFormattedBalance(address, denom) { + const response = await this.balance({ + address, + denom + }); + + // Extract amount or default to zero + const amount = response.balance?.amount || "0"; + + // Format the amount based on token type + if (denom.startsWith('u')) { + // Convert micro units to standard units (divide by 1,000,000) + const standardAmount = Number(amount) / 1_000_000; + // Format with two decimal places + const formattedAmount = standardAmount.toFixed(2); + // Get the actual token symbol (remove the 'u' prefix) + const symbol = denom.substring(1).toUpperCase(); + + return `${formattedAmount} ${symbol}`; + } + + // For other denominations, just return as is + return `${amount} ${denom}`; + } + + // Add a method to get all balances in a formatted way + async getFormattedBalances(address) { + const { balances } = await this.allBalances({ address }); + + return balances.map(coin => { + const { denom, amount } = coin; + + if (denom.startsWith('u')) { + const standardAmount = Number(amount) / 1_000_000; + const symbol = denom.substring(1).toUpperCase(); + return { denom: symbol, amount: standardAmount, formatted: `${standardAmount.toFixed(2)} ${symbol}` }; + } + + return { denom, amount: Number(amount), formatted: `${amount} ${denom}` }; + }); + } + + // Add a method to check if an account has sufficient balance + async hasSufficientBalance(address, requiredAmount, denom) { + const response = await this.balance({ + address, + denom + }); + + const balance = response.balance?.amount || "0"; + return BigInt(balance) >= BigInt(requiredAmount); + } +} + +// Usage example +async function useEnhancedBankClient() { + const baseUrl = "https://rest.cosmos.network"; + const axiosInstance = axios.create({ baseURL: baseUrl }); + + // Create our enhanced client + const bankClient = new EnhancedBankClient(axiosInstance, baseUrl); + + const address = "cosmos1..."; // Replace with a real address + + // Use the standard method + const { balance } = await bankClient.balance({ + address, + denom: "uatom" + }); + console.log("Raw balance:", balance); + + // Use the enhanced methods + const formattedBalance = await bankClient.getFormattedBalance(address, "uatom"); + console.log("Formatted balance:", formattedBalance); + + const allFormattedBalances = await bankClient.getFormattedBalances(address); + console.log("All formatted balances:", allFormattedBalances); + + const hasSufficientFunds = await bankClient.hasSufficientBalance(address, "1000000", "uatom"); + console.log("Has at least 1 ATOM:", hasSufficientFunds); +} +``` + +## Creating a Composite Client + +For applications that work with multiple modules, you can create a composite client that brings together functionality from different modules: + +```typescript +import { BankQueryClient } from "./codegen/cosmos/bank/v1beta1/query.lcd"; +import { StakingQueryClient } from "./codegen/cosmos/staking/v1beta1/query.lcd"; +import { DistributionQueryClient } from "./codegen/cosmos/distribution/v1beta1/query.lcd"; +import axios from "axios"; + +// Create a comprehensive account information client +class AccountInfoClient { + private bankClient: BankQueryClient; + private stakingClient: StakingQueryClient; + private distributionClient: DistributionQueryClient; + + constructor(restEndpoint: string) { + const axiosInstance = axios.create({ baseURL: restEndpoint }); + + this.bankClient = new BankQueryClient(axiosInstance, restEndpoint); + this.stakingClient = new StakingQueryClient(axiosInstance, restEndpoint); + this.distributionClient = new DistributionQueryClient(axiosInstance, restEndpoint); + } + + // Get comprehensive account overview + async getAccountOverview(address: string) { + // Execute all queries in parallel for efficiency + const [ + balancesResponse, + delegationsResponse, + unbondingResponse, + rewardsResponse + ] = await Promise.all([ + this.bankClient.allBalances({ address }), + this.stakingClient.delegatorDelegations({ delegatorAddr: address }), + this.stakingClient.delegatorUnbondingDelegations({ delegatorAddr: address }), + this.distributionClient.delegationTotalRewards({ delegatorAddress: address }) + ]); + + // Calculate total liquid balance + const liquidBalance = balancesResponse.balances.reduce( + (sum, coin) => sum + Number(coin.amount), + 0 + ); + + // Calculate total staked balance + const stakedBalance = delegationsResponse.delegationResponses.reduce( + (sum, del) => sum + Number(del.balance.amount), + 0 + ); + + // Calculate total unbonding balance + const unbondingBalance = unbondingResponse.unbondingResponses.reduce( + (sum, unbonding) => sum + unbonding.entries.reduce( + (entrySum, entry) => entrySum + Number(entry.balance), + 0 + ), + 0 + ); + + // Calculate total rewards + const totalRewards = rewardsResponse.total.reduce( + (sum, reward) => sum + Number(reward.amount), + 0 + ); + + // Return combined information + return { + address, + liquid: { + balances: balancesResponse.balances, + total: liquidBalance + }, + staked: { + delegations: delegationsResponse.delegationResponses, + total: stakedBalance + }, + unbonding: { + entries: unbondingResponse.unbondingResponses, + total: unbondingBalance + }, + rewards: { + details: rewardsResponse.rewards, + total: totalRewards + }, + netWorth: liquidBalance + stakedBalance + unbondingBalance + totalRewards + }; + } +} + +// Usage example +async function displayAccountOverview() { + const client = new AccountInfoClient("https://rest.cosmos.network"); + + const address = "cosmos1..."; // Replace with a real address + const overview = await client.getAccountOverview(address); + + console.log(`Account Overview for ${address}:`); + console.log(`Liquid Balance: ${overview.liquid.total / 1_000_000} ATOM`); + console.log(`Staked Balance: ${overview.staked.total / 1_000_000} ATOM`); + console.log(`Unbonding Balance: ${overview.unbonding.total / 1_000_000} ATOM`); + console.log(`Pending Rewards: ${overview.rewards.total / 1_000_000} ATOM`); + console.log(`Total Net Worth: ${overview.netWorth / 1_000_000} ATOM`); + + // If there are delegations, show the validators + if (overview.staked.delegations.length > 0) { + console.log("\nDelegations:"); + overview.staked.delegations.forEach(del => { + console.log(`- ${del.delegation.validatorAddress}: ${Number(del.balance.amount) / 1_000_000} ATOM`); + }); + } +} +``` + +## Adding Caching for Performance + +For production applications, you might want to add caching to avoid unnecessary network requests: + +```typescript +import { BankQueryClient } from "./codegen/cosmos/bank/v1beta1/query.lcd"; + +// Create a caching bank client +class CachingBankClient extends BankQueryClient { + private cache = new Map(); + private cacheTTL = 30 * 1000; // 30 seconds cache time-to-live + + // Helper to generate cache keys + private getCacheKey(method, params) { + return `${method}:${JSON.stringify(params)}`; + } + + // Override the balance method to add caching + async balance(params) { + const cacheKey = this.getCacheKey('balance', params); + + // Check if we have a cached response that's still valid + const cachedItem = this.cache.get(cacheKey); + if (cachedItem && Date.now() - cachedItem.timestamp < this.cacheTTL) { + console.log(`Cache hit for ${cacheKey}`); + return cachedItem.data; + } + + // No cache hit, call the actual method + console.log(`Cache miss for ${cacheKey}, fetching data...`); + const response = await super.balance(params); + + // Cache the response + this.cache.set(cacheKey, { + data: response, + timestamp: Date.now() + }); + + return response; + } + + // Similarly override other methods you want to cache + async allBalances(params) { + const cacheKey = this.getCacheKey('allBalances', params); + + const cachedItem = this.cache.get(cacheKey); + if (cachedItem && Date.now() - cachedItem.timestamp < this.cacheTTL) { + console.log(`Cache hit for ${cacheKey}`); + return cachedItem.data; + } + + console.log(`Cache miss for ${cacheKey}, fetching data...`); + const response = await super.allBalances(params); + + this.cache.set(cacheKey, { + data: response, + timestamp: Date.now() + }); + + return response; + } + + // Method to clear the cache + clearCache() { + this.cache.clear(); + console.log("Cache cleared"); + } + + // Method to set custom TTL + setCacheTTL(milliseconds) { + this.cacheTTL = milliseconds; + console.log(`Cache TTL set to ${milliseconds}ms`); + } +} + +// Example of using the caching client +async function demonstrateCaching() { + const baseUrl = "https://rest.cosmos.network"; + const axiosInstance = axios.create({ baseURL: baseUrl }); + + // Create caching client + const bankClient = new CachingBankClient(axiosInstance, baseUrl); + + const address = "cosmos1..."; // Replace with a real address + + console.log("First request (will fetch from network):"); + await bankClient.balance({ address, denom: "uatom" }); + + console.log("\nSecond request (should use cache):"); + await bankClient.balance({ address, denom: "uatom" }); + + console.log("\nDifferent request (will fetch from network):"); + await bankClient.balance({ address, denom: "uosmo" }); + + console.log("\nClearing cache..."); + bankClient.clearCache(); + + console.log("\nRequest after clearing cache (will fetch from network):"); + await bankClient.balance({ address, denom: "uatom" }); +} +``` + +## Adding Error Handling and Retries + +For more robust applications, you might want to add error handling and retry logic: + +```typescript +import { StakingQueryClient } from "./codegen/cosmos/staking/v1beta1/query.lcd"; + +// Create a client with error handling and retries +class RobustStakingClient extends StakingQueryClient { + private maxRetries = 3; + private baseDelay = 1000; // 1 second initial delay + + // Helper method to handle retries + private async withRetry(method, params) { + let lastError; + + for (let attempt = 1; attempt <= this.maxRetries; attempt++) { + try { + // Call the parent method + return await method.call(this, params); + } catch (error) { + lastError = error; + + // Check if we should retry based on the error + if (this.isRetryableError(error) && attempt < this.maxRetries) { + const delay = this.calculateBackoff(attempt); + console.warn(`Request failed (attempt ${attempt}/${this.maxRetries}). Retrying in ${delay}ms...`); + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + // We've exhausted retries or it's not a retryable error + throw error; + } + } + } + + throw lastError; + } + + // Determine if an error should trigger a retry + private isRetryableError(error) { + // Network errors or server errors (5xx) are retryable + return ( + !error.response || // Network error + (error.response && error.response.status >= 500) // Server error + ); + } + + // Calculate exponential backoff + private calculateBackoff(attempt) { + return this.baseDelay * Math.pow(2, attempt - 1); + } + + // Override methods to add retry logic + async validators(params) { + return this.withRetry(super.validators, params); + } + + async validator(params) { + return this.withRetry(super.validator, params); + } + + async delegatorDelegations(params) { + return this.withRetry(super.delegatorDelegations, params); + } + + // Override other methods similarly... +} + +// Usage example +async function demonstrateRetries() { + const baseUrl = "https://rest.cosmos.network"; + const axiosInstance = axios.create({ + baseURL: baseUrl, + timeout: 3000 // Short timeout to demonstrate timeouts + }); + + const stakingClient = new RobustStakingClient(axiosInstance, baseUrl); + + try { + const { validators } = await stakingClient.validators({}); + console.log(`Successfully retrieved ${validators.length} validators`); + } catch (error) { + console.error("All retries failed:", error.message); + } +} +``` + +## Creating a Complete Dashboard App + +Let's put everything together into a complete example – a dashboard app that displays blockchain statistics: + +```typescript +import axios from "axios"; +import { + BankQueryClient, + StakingQueryClient, + GovQueryClient, + MintQueryClient, + TendermintQueryClient +} from "./codegen"; + +// Blockchain dashboard client with caching, retries, and error handling +class BlockchainDashboardClient { + private bank: BankQueryClient; + private staking: StakingQueryClient; + private gov: GovQueryClient; + private mint: MintQueryClient; + private tendermint: TendermintQueryClient; + + private cache = new Map(); + private cacheTTL = 60 * 1000; // 1 minute + + constructor(restEndpoint: string) { + const axiosInstance = axios.create({ + baseURL: restEndpoint, + timeout: 10000 + }); + + // Add response interceptor for error handling + axiosInstance.interceptors.response.use( + response => response, + error => { + console.error(`API Error: ${error.message}`); + return Promise.reject(error); + } + ); + + this.bank = new BankQueryClient(axiosInstance, restEndpoint); + this.staking = new StakingQueryClient(axiosInstance, restEndpoint); + this.gov = new GovQueryClient(axiosInstance, restEndpoint); + this.mint = new MintQueryClient(axiosInstance, restEndpoint); + this.tendermint = new TendermintQueryClient(axiosInstance, restEndpoint); + } + + // Helper for caching + private async getCachedData(key, fetcher) { + const cachedItem = this.cache.get(key); + if (cachedItem && Date.now() - cachedItem.timestamp < this.cacheTTL) { + return cachedItem.data; + } + + const data = await fetcher(); + this.cache.set(key, { + data, + timestamp: Date.now() + }); + + return data; + } + + // Get blockchain overview + async getDashboardData() { + return this.getCachedData('dashboard', async () => { + try { + // Run all queries in parallel + const [ + nodeInfo, + latestBlock, + validatorsResponse, + activeProposalsResponse, + supplyResponse, + inflationResponse + ] = await Promise.all([ + this.tendermint.getNodeInfo({}), + this.tendermint.getLatestBlock({}), + this.staking.validators({ status: "BOND_STATUS_BONDED" }), + this.gov.proposals({ proposalStatus: 2 }), // PROPOSAL_STATUS_VOTING_PERIOD + this.bank.totalSupply({}), + this.mint.inflation({}) + ]); + + // Process the data + const chainId = nodeInfo.defaultNodeInfo.network; + const blockHeight = latestBlock.block.header.height; + const blockTime = new Date(latestBlock.block.header.time); + + const activeValidators = validatorsResponse.validators; + const totalVotingPower = activeValidators.reduce( + (sum, v) => sum + BigInt(v.tokens), + BigInt(0) + ); + + const topValidators = activeValidators + .sort((a, b) => Number(BigInt(b.tokens) - BigInt(a.tokens))) + .slice(0, 10) + .map(v => ({ + name: v.description.moniker, + address: v.operatorAddress, + votingPower: Number(BigInt(v.tokens) * BigInt(100) / totalVotingPower), + commission: parseFloat(v.commission.commissionRates.rate) * 100 + })); + + const activeProposals = activeProposalsResponse.proposals.map(p => ({ + id: p.proposalId, + title: p.content.title, + description: p.content.description, + votingEndTime: p.votingEndTime + })); + + const atomSupply = supplyResponse.supply.find(c => c.denom === "uatom"); + const totalAtoms = atomSupply + ? Number(atomSupply.amount) / 1_000_000 + : 0; + + const inflation = parseFloat(inflationResponse.inflation) * 100; + + return { + chainId, + blockHeight, + blockTime, + validators: { + active: activeValidators.length, + top: topValidators + }, + governance: { + activeProposals + }, + economics: { + totalSupply: totalAtoms, + inflation + }, + lastUpdated: new Date() + }; + } catch (error) { + console.error("Error fetching dashboard data:", error); + throw new Error(`Failed to fetch dashboard data: ${error.message}`); + } + }); + } + + // Clear the cache + clearCache() { + this.cache.clear(); + } +} + +// Usage example +async function showDashboard() { + const client = new BlockchainDashboardClient("https://rest.cosmos.network"); + + try { + const data = await client.getDashboardData(); + + console.log(`=== ${data.chainId} Dashboard ===`); + console.log(`Block Height: ${data.blockHeight}`); + console.log(`Latest Block: ${data.blockTime}`); + console.log(`Active Validators: ${data.validators.active}`); + console.log(`Current Inflation: ${data.economics.inflation.toFixed(2)}%`); + console.log(`Total ATOM Supply: ${data.economics.totalSupply.toLocaleString()} ATOM`); + + console.log("\nTop Validators:"); + data.validators.top.forEach((v, i) => { + console.log(`${i+1}. ${v.name} (${v.votingPower.toFixed(2)}%) - Commission: ${v.commission.toFixed(2)}%`); + }); + + if (data.governance.activeProposals.length > 0) { + console.log("\nActive Governance Proposals:"); + data.governance.activeProposals.forEach(p => { + console.log(`#${p.id}: ${p.title}`); + console.log(` Voting ends: ${new Date(p.votingEndTime).toLocaleString()}`); + }); + } else { + console.log("\nNo active governance proposals"); + } + + console.log(`\nLast updated: ${data.lastUpdated.toLocaleString()}`); + } catch (error) { + console.error("Failed to show dashboard:", error.message); + } +} + +// Run the dashboard +showDashboard(); +``` + +## Conclusion + +In this tutorial, you've learned how to: + +1. **Enable and generate LCD Client Classes** with Telescope +2. **Use the basic client classes** to query blockchain data +3. **Extend the client classes** with custom methods +4. **Create composite clients** by combining multiple module clients +5. **Add caching** for better performance +6. **Implement error handling and retry logic** for more robust applications +7. **Create a complete dashboard application** using all these techniques + +LCD Client Classes provide a powerful foundation for building Cosmos applications. By extending and combining these classes, you can create reusable, maintainable code that interacts with any Cosmos SDK blockchain. + +In the next tutorial, we'll explore [RPC Clients](./rpc-clients.md) for more direct and powerful interactions with blockchain nodes. \ No newline at end of file diff --git a/learn/lcd-clients.mdx b/learn/lcd-clients.mdx new file mode 100644 index 0000000000..fdc23bc762 --- /dev/null +++ b/learn/lcd-clients.mdx @@ -0,0 +1,495 @@ +# Using LCD Clients + +In this tutorial, we'll learn how to use LCD (Light Client Daemon) clients with Telescope-generated code to query blockchain data through REST endpoints. This approach is ideal for applications that need to read data from a blockchain without submitting transactions. + +## Understanding LCD Clients + +LCD clients allow your application to query the state of a blockchain through REST API endpoints. These endpoints provide access to all the data stored on the blockchain, from account balances to validator information to governance proposals. + +The advantage of using Telescope-generated LCD clients is that they provide full TypeScript type safety, making your queries more reliable and providing excellent autocomplete support in your IDE. + +## Setting Up Your Project + +First, let's make sure your Telescope configuration has LCD client generation enabled: + +```typescript +// In your Telescope configuration file +import { TelescopeOptions } from "@cosmology/types"; + +const options: TelescopeOptions = { + lcdClients: { + enabled: true + } + // Other Telescope options... +}; + +export default options; +``` + +Run your Telescope code generation, and it will create LCD client code for all the modules in your Protobuf definitions. + +## Creating a Basic LCD Client + +Let's start by creating a simple LCD client to query the Cosmos Hub: + +```typescript +import { createLCDClient } from "./codegen/client"; + +async function queryCosmosHub() { + // Create an LCD client connected to the Cosmos Hub REST endpoint + const client = await createLCDClient({ + restEndpoint: "https://rest.cosmos.network" + }); + + console.log("Successfully connected to Cosmos Hub REST API"); + + // Now you can use this client to query the blockchain + return client; +} + +// Call the function to get our client +const client = await queryCosmosHub(); +``` + +Now that we have our client, let's learn how to use it to query blockchain data. + +## Querying Account Balances + +One of the most common operations is checking an account's balance: + +```typescript +async function checkAccountBalance(address) { + const client = await createLCDClient({ + restEndpoint: "https://rest.cosmos.network" + }); + + try { + // Query all balances for an address + const { balances } = await client.cosmos.bank.v1beta1.allBalances({ + address: address + }); + + console.log(`Account ${address} has the following balances:`); + + if (balances.length === 0) { + console.log(" No tokens found"); + } else { + balances.forEach(coin => { + console.log(` ${coin.amount} ${coin.denom}`); + }); + } + + return balances; + } catch (error) { + console.error("Error querying balances:", error.message); + throw error; + } +} + +// Example usage +const address = "cosmos1..."; // Replace with a real address +const balances = await checkAccountBalance(address); +``` + +## Querying Validators + +Let's see how to get information about active validators: + +```typescript +async function getActiveValidators() { + const client = await createLCDClient({ + restEndpoint: "https://rest.cosmos.network" + }); + + try { + // Query active validators (bonded) + const { validators } = await client.cosmos.staking.v1beta1.validators({ + status: "BOND_STATUS_BONDED" + }); + + console.log(`Found ${validators.length} active validators:`); + + // Print top 5 validators by voting power + validators + .sort((a, b) => parseInt(b.tokens) - parseInt(a.tokens)) + .slice(0, 5) + .forEach((validator, index) => { + console.log(`${index + 1}. ${validator.description.moniker}`); + console.log(` Address: ${validator.operatorAddress}`); + console.log(` Voting Power: ${parseInt(validator.tokens) / 1000000} ATOM`); + console.log(` Commission: ${parseFloat(validator.commission.commissionRates.rate) * 100}%`); + }); + + return validators; + } catch (error) { + console.error("Error querying validators:", error.message); + throw error; + } +} + +// Get active validators +const validators = await getActiveValidators(); +``` + +## Working with Pagination + +When querying large datasets, you'll need to handle pagination to get all the results: + +```typescript +async function getAllProposals() { + const client = await createLCDClient({ + restEndpoint: "https://rest.cosmos.network" + }); + + let allProposals = []; + let nextKey = null; + + try { + // Loop to get all pages of data + do { + // Set up pagination parameters + const paginationParams = nextKey + ? { key: nextKey, limit: "50" } + : { limit: "50" }; + + // Query proposals with pagination + const response = await client.cosmos.gov.v1beta1.proposals({ + pagination: paginationParams, + proposalStatus: 0, // 0 means all proposals + }); + + // Add this page of proposals to our result + allProposals = [...allProposals, ...response.proposals]; + + // Update nextKey for the next iteration + nextKey = response.pagination?.nextKey || null; + + console.log(`Fetched ${response.proposals.length} proposals, total: ${allProposals.length}`); + + } while (nextKey); + + console.log(`Retrieved a total of ${allProposals.length} proposals`); + return allProposals; + } catch (error) { + console.error("Error querying proposals:", error.message); + throw error; + } +} + +// Get all governance proposals +const proposals = await getAllProposals(); +``` + +## Querying Chain-specific Modules + +Different Cosmos chains have their own specific modules. Let's look at an example of querying Osmosis liquidity pools: + +```typescript +async function getOsmosisPools() { + const client = await createLCDClient({ + restEndpoint: "https://rest.osmosis.zone" + }); + + try { + // Query pools on Osmosis + const { pools } = await client.osmosis.gamm.v1beta1.pools({}); + + console.log(`Found ${pools.length} liquidity pools on Osmosis`); + + // Look at details of pool #1 (a popular ATOM/OSMO pool) + const { pool } = await client.osmosis.gamm.v1beta1.pool({ + poolId: "1" + }); + + console.log("Pool #1 details:"); + console.log(` ID: ${pool.id}`); + console.log(` Type: ${pool["@type"]}`); + console.log(" Assets:"); + + // Display the assets in the pool + if (pool.poolAssets) { + pool.poolAssets.forEach(asset => { + console.log(` ${asset.token.amount} ${asset.token.denom}`); + }); + } + + return { + pools, + pool1Details: pool + }; + } catch (error) { + console.error("Error querying Osmosis pools:", error.message); + throw error; + } +} + +// Get information about Osmosis pools +const osmosisData = await getOsmosisPools(); +``` + +## Creating Custom Scoped Clients + +For larger applications, you might want to create scoped clients that only include the modules you need: + +```typescript +// This is part of your Telescope configuration file +const options: TelescopeOptions = { + lcdClients: { + enabled: true, + scoped: [ + { + dir: "bank-client", + filename: "bank-client.ts", + packages: [ + "cosmos.bank.v1beta1", + "cosmos.base.v1beta1" + ], + addToBundle: true, + methodName: "createBankClient" + } + ] + } +}; + +// After running Telescope, you can use your scoped client: +import { createBankClient } from "./codegen/bank-client/bank-client"; + +async function queryBankModule() { + const bankClient = await createBankClient({ + restEndpoint: "https://rest.cosmos.network" + }); + + // Now you can only access the bank module + const { balances } = await bankClient.cosmos.bank.v1beta1.allBalances({ + address: "cosmos1..." + }); + + // This would fail because the client only has bank module + // const validators = await bankClient.cosmos.staking.v1beta1.validators({}); + + return balances; +} +``` + +## Error Handling Best Practices + +When working with network requests, it's important to handle errors properly: + +```typescript +async function queryWithErrorHandling(address) { + const client = await createLCDClient({ + restEndpoint: "https://rest.cosmos.network" + }); + + try { + const response = await client.cosmos.bank.v1beta1.balance({ + address: address, + denom: "uatom" + }); + + return response.balance; + } catch (error) { + // Handle different types of errors + if (error.response) { + // The server responded with an error status + if (error.response.status === 400) { + console.error("Bad request. Check your parameters:", error.response.data); + } else if (error.response.status === 404) { + console.error("Resource not found. The address may not exist on chain."); + } else if (error.response.status === 500) { + console.error("Server error. The node might be experiencing issues."); + } else { + console.error(`Error ${error.response.status}:`, error.response.data); + } + } else if (error.request) { + // The request was made but no response received + console.error("No response from server. Check your internet connection or the node may be down."); + } else { + // Something happened in setting up the request + console.error("Error creating request:", error.message); + } + + // You might want to return a default value or rethrow + throw new Error(`Failed to query balance for ${address}: ${error.message}`); + } +} +``` + +## Building a Dashboard Application + +Let's put everything together and create a simple blockchain dashboard: + +```typescript +import { createLCDClient } from "./codegen/client"; + +async function createBlockchainDashboard() { + // Create the client + const client = await createLCDClient({ + restEndpoint: "https://rest.cosmos.network" + }); + + try { + // Get chain information + const nodeInfo = await client.cosmos.base.tendermint.v1beta1.getNodeInfo({}); + const chainId = nodeInfo.defaultNodeInfo.network; + console.log(`Connected to chain: ${chainId}`); + + // Get latest block + const latestBlock = await client.cosmos.base.tendermint.v1beta1.getLatestBlock({}); + console.log(`Latest block: ${latestBlock.block.header.height}`); + console.log(`Block time: ${latestBlock.block.header.time}`); + + // Get validator count + const { validators: activeValidators } = await client.cosmos.staking.v1beta1.validators({ + status: "BOND_STATUS_BONDED" + }); + console.log(`Active validators: ${activeValidators.length}`); + + // Get governance metrics + const { proposals } = await client.cosmos.gov.v1beta1.proposals({ + proposalStatus: 2 // PROPOSAL_STATUS_VOTING_PERIOD + }); + console.log(`Proposals in voting period: ${proposals.length}`); + + // Get supply information + const totalSupply = await client.cosmos.bank.v1beta1.totalSupply({}); + const atomSupply = totalSupply.supply.find(coin => coin.denom === "uatom"); + if (atomSupply) { + const atomAmount = parseInt(atomSupply.amount) / 1000000; // Convert from uatom to ATOM + console.log(`Total ATOM supply: ${atomAmount.toLocaleString()} ATOM`); + } + + // Get inflation rate + const inflation = await client.cosmos.mint.v1beta1.inflation({}); + const inflationRate = parseFloat(inflation.inflation) * 100; + console.log(`Current inflation rate: ${inflationRate.toFixed(2)}%`); + + return { + chainId, + latestBlockHeight: latestBlock.block.header.height, + blockTime: latestBlock.block.header.time, + activeValidatorCount: activeValidators.length, + proposalsInVoting: proposals.length, + atomSupply: atomSupply ? parseInt(atomSupply.amount) / 1000000 : 0, + inflationRate: inflationRate + }; + } catch (error) { + console.error("Error creating dashboard:", error.message); + throw error; + } +} + +// Create the dashboard +const dashboard = await createBlockchainDashboard(); +console.log(JSON.stringify(dashboard, null, 2)); +``` + +## Working with CosmWasm Smart Contracts + +If you're working with a chain that supports CosmWasm smart contracts, you can query them like this: + +```typescript +async function querySmartContract() { + // Connect to a CosmWasm-enabled chain like Juno + const client = await createLCDClient({ + restEndpoint: "https://rest.juno.strange.love" + }); + + try { + // The contract address you want to query + const contractAddress = "juno1..."; // Replace with a real contract address + + // The query to send to the contract (this depends on the contract's interface) + const queryMsg = { + token_info: {} + }; + + // Convert the JSON query to base64 as required by the API + const queryData = btoa(JSON.stringify(queryMsg)); + + // Query the smart contract + const response = await client.cosmwasm.wasm.v1.smartContractState({ + address: contractAddress, + queryData: queryData + }); + + // The data comes back as binary, so we need to decode and parse it + const contractData = JSON.parse( + new TextDecoder().decode(response.data) + ); + + console.log("Contract query result:", contractData); + return contractData; + } catch (error) { + console.error("Error querying smart contract:", error.message); + throw error; + } +} + +// Query a CosmWasm smart contract +const contractData = await querySmartContract(); +``` + +## Performance Optimization Tips + +When working with LCD clients in production applications, consider these tips: + +```typescript +// Create a client with custom timeout and caching headers +import { createLCDClient } from "./codegen/client"; +import axios from "axios"; + +// Create a custom axios instance with caching +const axiosInstance = axios.create({ + timeout: 10000, // 10 seconds timeout + headers: { + "Cache-Control": "max-age=30" // Allow caching for 30 seconds + } +}); + +// Create the LCD client with the custom axios instance +const client = await createLCDClient({ + restEndpoint: "https://rest.cosmos.network", + // Pass the custom axios instance + axios: axiosInstance +}); + +// Example of batching multiple queries together for efficiency +async function batchQueries(address) { + // Use Promise.all to run queries in parallel + const [balanceResponse, delegationsResponse, rewardsResponse] = await Promise.all([ + client.cosmos.bank.v1beta1.allBalances({ address }), + client.cosmos.staking.v1beta1.delegatorDelegations({ delegatorAddr: address }), + client.cosmos.distribution.v1beta1.delegationTotalRewards({ delegatorAddress: address }) + ]); + + // Now use the results + console.log("Balances:", balanceResponse.balances); + console.log("Delegations:", delegationsResponse.delegationResponses); + console.log("Rewards:", rewardsResponse.rewards); + + return { + balances: balanceResponse.balances, + delegations: delegationsResponse.delegationResponses, + rewards: rewardsResponse.rewards + }; +} +``` + +## Conclusion + +In this tutorial, you've learned how to: + +1. Enable LCD client generation in Telescope +2. Create and use LCD clients for querying blockchain data +3. Handle pagination for large datasets +4. Work with chain-specific modules like Osmosis DEX +5. Create custom scoped clients for your application +6. Properly handle errors in network requests +7. Create a simple blockchain dashboard using various queries +8. Query CosmWasm smart contracts +9. Optimize performance for production applications + +LCD clients provide a convenient way to read data from the blockchain without having to set up a full node or use more complex RPC methods. They're perfect for applications that need to display blockchain data to users or perform read-only operations. + +In the next tutorial, we'll explore [LCD Client Classes](./lcd-clients-classes.md) for more advanced usage patterns. \ No newline at end of file diff --git a/learn/manually-registering-types.mdx b/learn/manually-registering-types.mdx new file mode 100644 index 0000000000..4d19ea4308 --- /dev/null +++ b/learn/manually-registering-types.mdx @@ -0,0 +1,659 @@ +# Manual Type Registration + +In this tutorial, we'll explore how to manually register custom types with Telescope-generated code. This is a powerful technique that allows you to extend the capabilities of Telescope by integrating custom or third-party types with the generated TypeScript code. + +## Understanding Type Registration + +When working with Cosmos SDK blockchains, all messages and data structures are defined as Protocol Buffer (protobuf) types. Telescope automatically generates TypeScript representations of these types from protobuf definitions, and registers them in a type registry system that enables serialization, deserialization, and type identification. + +However, there are scenarios where you might need to work with types that weren't generated by Telescope: + +1. Custom types you've created manually +2. Types from third-party libraries +3. Types for chains or modules not included in your protobuf definitions +4. Specialized types for particular use cases + +In these situations, you need to manually register these types to make them work seamlessly with the rest of your Telescope-generated code. + +## How Type Registration Works + +To understand manual type registration, let's first look at how Telescope registers types automatically. For each message type defined in protobuf, Telescope generates: + +1. A TypeScript interface representing the type's structure +2. Methods for encoding, decoding, and creating instances of the type +3. A type URL that uniquely identifies the type (e.g., `/cosmos.bank.v1beta1.MsgSend`) + +These types are then collected into registries that pair each type URL with its corresponding implementation: + +```typescript +export const registry = [ + ["/cosmos.bank.v1beta1.MsgSend", MsgSend], + ["/cosmos.staking.v1beta1.MsgDelegate", MsgDelegate], + // other message types... +]; +``` + +When manually registering types, we're essentially doing the same thing - creating the necessary TypeScript implementation, and adding it to a registry for use in our application. + +## Creating a Custom Type + +Let's start by creating a custom message type from scratch. For this example, we'll create a simple `CustomMessage` type that might be used in a custom module: + +```typescript +// my-custom-types.ts +import { Writer, Reader } from "protobufjs/minimal"; + +// Define the interface for our custom type +export interface CustomMessage { + sender: string; + recipient: string; + amount: number; + memo: string; +} + +// Create the implementation that satisfies the GeneratedType interface +export const CustomMessage = { + // The unique type URL + typeUrl: "/my.custom.v1beta1.CustomMessage", + + // Encode method: converts the message into binary format + encode(message: CustomMessage, writer: Writer = Writer.create()): Writer { + if (message.sender !== "") { + writer.uint32(10).string(message.sender); + } + if (message.recipient !== "") { + writer.uint32(18).string(message.recipient); + } + if (message.amount !== 0) { + writer.uint32(24).uint64(message.amount); + } + if (message.memo !== "") { + writer.uint32(34).string(message.memo); + } + return writer; + }, + + // Decode method: converts binary back into the message object + decode(input: Reader | Uint8Array, length?: number): CustomMessage { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { + sender: "", + recipient: "", + amount: 0, + memo: "" + } as CustomMessage; + + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.sender = reader.string(); + break; + case 2: + message.recipient = reader.string(); + break; + case 3: + message.amount = reader.uint64().toNumber(); + break; + case 4: + message.memo = reader.string(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + + return message; + }, + + // fromPartial method: creates a message from a partial object + fromPartial(object: DeepPartial): CustomMessage { + const message = { + sender: "", + recipient: "", + amount: 0, + memo: "" + } as CustomMessage; + + if (object.sender !== undefined && object.sender !== null) { + message.sender = object.sender; + } + if (object.recipient !== undefined && object.recipient !== null) { + message.recipient = object.recipient; + } + if (object.amount !== undefined && object.amount !== null) { + message.amount = object.amount; + } + if (object.memo !== undefined && object.memo !== null) { + message.memo = object.memo; + } + + return message; + } +}; + +// Helper type for partial objects +type DeepPartial = { + [P in keyof T]?: T[P] extends Array + ? Array> + : T[P] extends ReadonlyArray + ? ReadonlyArray> + : DeepPartial | T[P]; +}; +``` + +This custom type implementation mimics what Telescope would generate, providing methods for encoding, decoding, and creating message instances. The key difference is that we've written it manually instead of having it generated from protobuf. + +## Registering a Custom Type + +Once we have our custom type, we need to register it to make it usable with CosmJS and Telescope-generated clients. There are several ways to do this: + +### Method 1: Adding to an Existing Registry + +The simplest approach is to add our custom type to an existing registry: + +```typescript +import { registry as bankRegistry } from "./codegen/cosmos/bank/v1beta1/tx"; +import { GeneratedType } from "@cosmjs/proto-signing"; +import { CustomMessage } from "./my-custom-types"; + +// Create a new extended registry that includes both the original types and our custom type +const extendedRegistry: Array<[string, GeneratedType]> = [ + ...bankRegistry, + [CustomMessage.typeUrl, CustomMessage] +]; + +// Use this extended registry when creating clients +import { SigningStargateClient } from "@cosmjs/stargate"; + +async function createClientWithCustomType() { + const client = await SigningStargateClient.connectWithSigner( + "https://rpc.cosmos.network", + signer, + { registry: extendedRegistry } + ); + + // Now you can use your custom types with this client + return client; +} +``` + +### Method 2: Using the Registry Class + +A more flexible approach is to use the `Registry` class from CosmJS, which allows you to add types dynamically: + +```typescript +import { Registry } from "@cosmjs/proto-signing"; +import { defaultRegistryTypes } from "@cosmjs/stargate"; +import { CustomMessage } from "./my-custom-types"; + +function createRegistryWithCustomTypes() { + // Start with the default registry types + const registry = new Registry(defaultRegistryTypes); + + // Add our custom type + registry.register(CustomMessage.typeUrl, CustomMessage); + + return registry; +} + +// Use the registry +import { SigningStargateClient } from "@cosmjs/stargate"; + +async function createClient() { + const registry = createRegistryWithCustomTypes(); + + const client = await SigningStargateClient.connectWithSigner( + "https://rpc.cosmos.network", + signer, + { registry } + ); + + return client; +} +``` + +### Method 3: Creating a Module Registration Function + +For larger applications with many custom types, it's often useful to organize them into module-based registration functions: + +```typescript +import { Registry } from "@cosmjs/proto-signing"; +import { + CustomMessage, + AnotherCustomType, + YetAnotherType +} from "./my-custom-types"; + +// Create a registration function for all types in your custom module +export function registerMyCustomModuleTypes(registry: Registry): void { + registry.register(CustomMessage.typeUrl, CustomMessage); + registry.register(AnotherCustomType.typeUrl, AnotherCustomType); + registry.register(YetAnotherType.typeUrl, YetAnotherType); +} + +// Using the registration function +import { Registry } from "@cosmjs/proto-signing"; +import { defaultRegistryTypes } from "@cosmjs/stargate"; + +function createRegistry() { + const registry = new Registry(defaultRegistryTypes); + registerMyCustomModuleTypes(registry); + return registry; +} +``` + +This approach is modular and maintainable, making it easy to add or remove entire sets of custom types. + +## Working with Amino Types + +If you're building a DApp that needs to work with wallets (like Keplr) that still use the legacy Amino encoding, you'll also need to register Amino converters for your custom types: + +```typescript +import { AminoTypes, AminoConverters } from "@cosmjs/stargate"; +import { AminoMsg } from "@cosmjs/amino"; +import { CustomMessage } from "./my-custom-types"; + +// Define the Amino interface for your custom message +interface AminoCustomMessage extends AminoMsg { + type: "my-module/CustomMessage"; + value: { + sender: string; + recipient: string; + amount: string; // Note: numbers are strings in Amino + memo: string; + }; +} + +// Create the Amino converters +const myCustomAminoConverters: AminoConverters = { + [CustomMessage.typeUrl]: { + aminoType: "my-module/CustomMessage", + toAmino: ({ sender, recipient, amount, memo }: CustomMessage): AminoCustomMessage["value"] => ({ + sender, + recipient, + amount: amount.toString(), // Convert number to string for Amino + memo, + }), + fromAmino: ({ sender, recipient, amount, memo }: AminoCustomMessage["value"]): CustomMessage => ({ + sender, + recipient, + amount: parseInt(amount, 10), // Convert string back to number + memo, + }), + }, +}; + +// Use the Amino converters with a client +import { SigningStargateClient } from "@cosmjs/stargate"; + +async function createClient() { + // Create a registry with our custom types + const registry = createRegistryWithCustomTypes(); + + // Create Amino types with our converters + const aminoTypes = new AminoTypes(myCustomAminoConverters); + + // Use both in the client + const client = await SigningStargateClient.connectWithSigner( + "https://rpc.cosmos.network", + signer, + { + registry, + aminoTypes, + } + ); + + return client; +} +``` + +## Using the Global Decoder Registry + +Telescope version 1.0 and later includes a global decoder registry system that can simplify working with different message types: + +```typescript +import { GlobalDecoderRegistry } from "./codegen/registry"; +import { CustomMessage } from "./my-custom-types"; + +// Register your custom type +GlobalDecoderRegistry.register(CustomMessage.typeUrl, CustomMessage); + +// Later, when you need to decode a message +function decodeAnyMessage(typeUrl: string, binaryData: Uint8Array) { + return GlobalDecoderRegistry.decode({ + typeUrl, + value: binaryData + }); +} +``` + +This is particularly useful when you're dealing with messages of different types and don't know the exact type in advance. + +## A Complete Example: Custom NFT Message + +Let's put everything together in a complete example. We'll create a custom NFT transfer message, register it, and use it in a transaction: + +```typescript +// nft-types.ts +import { Writer, Reader } from "protobufjs/minimal"; + +// Define our custom NFT transfer message +export interface MsgTransferNFT { + sender: string; + recipient: string; + collectionId: string; + tokenId: string; +} + +// Implement the methods +export const MsgTransferNFT = { + typeUrl: "/my.nft.v1beta1.MsgTransferNFT", + + encode(message: MsgTransferNFT, writer: Writer = Writer.create()): Writer { + if (message.sender !== "") { + writer.uint32(10).string(message.sender); + } + if (message.recipient !== "") { + writer.uint32(18).string(message.recipient); + } + if (message.collectionId !== "") { + writer.uint32(26).string(message.collectionId); + } + if (message.tokenId !== "") { + writer.uint32(34).string(message.tokenId); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): MsgTransferNFT { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { + sender: "", + recipient: "", + collectionId: "", + tokenId: "" + } as MsgTransferNFT; + + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.sender = reader.string(); + break; + case 2: + message.recipient = reader.string(); + break; + case 3: + message.collectionId = reader.string(); + break; + case 4: + message.tokenId = reader.string(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + + return message; + }, + + fromPartial(object: DeepPartial): MsgTransferNFT { + const message = { + sender: "", + recipient: "", + collectionId: "", + tokenId: "" + } as MsgTransferNFT; + + if (object.sender !== undefined && object.sender !== null) { + message.sender = object.sender; + } + if (object.recipient !== undefined && object.recipient !== null) { + message.recipient = object.recipient; + } + if (object.collectionId !== undefined && object.collectionId !== null) { + message.collectionId = object.collectionId; + } + if (object.tokenId !== undefined && object.tokenId !== null) { + message.tokenId = object.tokenId; + } + + return message; + } +}; + +// Also define the Amino converter +import { AminoMsg } from "@cosmjs/amino"; + +export interface AminoMsgTransferNFT extends AminoMsg { + type: "my-nft/TransferNFT"; + value: { + sender: string; + recipient: string; + collection_id: string; // Note: snake_case in Amino + token_id: string; + }; +} + +export const aminoConverters = { + [MsgTransferNFT.typeUrl]: { + aminoType: "my-nft/TransferNFT", + toAmino: ({ sender, recipient, collectionId, tokenId }: MsgTransferNFT): AminoMsgTransferNFT["value"] => ({ + sender, + recipient, + collection_id: collectionId, // Convert to snake_case + token_id: tokenId, + }), + fromAmino: ({ sender, recipient, collection_id, token_id }: AminoMsgTransferNFT["value"]): MsgTransferNFT => ({ + sender, + recipient, + collectionId: collection_id, // Convert back to camelCase + tokenId: token_id, + }), + }, +}; + +// Helper type +type DeepPartial = { + [P in keyof T]?: T[P] extends Array + ? Array> + : T[P] extends ReadonlyArray + ? ReadonlyArray> + : DeepPartial | T[P]; +}; +``` + +Now, let's use this custom NFT message in an application: + +```typescript +// nft-app.ts +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { SigningStargateClient, AminoTypes, defaultRegistryTypes } from "@cosmjs/stargate"; +import { Registry } from "@cosmjs/proto-signing"; +import { MsgTransferNFT, aminoConverters } from "./nft-types"; + +async function transferNFT( + mnemonic: string, + recipientAddress: string, + collectionId: string, + tokenId: string +) { + try { + // Create a wallet + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { + prefix: "cosmos", // Use the appropriate address prefix for your chain + }); + + // Get the sender address + const [account] = await wallet.getAccounts(); + const sender = account.address; + + // Create a registry with our custom NFT message type + const registry = new Registry(defaultRegistryTypes); + registry.register(MsgTransferNFT.typeUrl, MsgTransferNFT); + + // Set up Amino types for legacy wallet support + const aminoTypes = new AminoTypes(aminoConverters); + + // Create a signing client + const client = await SigningStargateClient.connectWithSigner( + "https://rpc.your-cosmos-chain.com", // Replace with your chain's RPC endpoint + wallet, + { registry, aminoTypes } + ); + + // Prepare the NFT transfer message + const transferMsg = { + typeUrl: MsgTransferNFT.typeUrl, + value: MsgTransferNFT.fromPartial({ + sender, + recipient: recipientAddress, + collectionId, + tokenId, + }), + }; + + // Define the fee + const fee = { + amount: [{ denom: "uatom", amount: "5000" }], // Use appropriate denom and amount + gas: "200000", + }; + + // Broadcast the transaction + console.log("Sending NFT transfer transaction..."); + const result = await client.signAndBroadcast( + sender, + [transferMsg], + fee, + "Transferring NFT" + ); + + // Check the result + if (result.code === 0) { + console.log("NFT transfer successful!"); + console.log("Transaction hash:", result.transactionHash); + } else { + console.error("NFT transfer failed:", result.rawLog); + } + + return result; + } catch (error) { + console.error("Error transferring NFT:", error); + throw error; + } +} + +// Example usage +transferNFT( + "your mnemonic phrase here", + "cosmos1recipient...", + "my-awesome-collection-1", + "token-123" +).catch(console.error); +``` + +## Tips and Best Practices + +When manually registering types, follow these best practices for the best results: + +### 1. Follow Protobuf Encoding Rules + +Your manual encode and decode methods should follow the same protobuf encoding rules as generated code. This includes field numbers, wire types, and appropriate type handling. + +### 2. Use Consistent Type URLs + +Choose type URLs that follow the standard pattern: +- `/namespace.module.version.TypeName` +- Example: `/my.nft.v1beta1.MsgTransferNFT` + +### 3. Organize Types by Module + +Group related types together in module-specific files and registration functions for better organization: + +```typescript +// Register all bank-related types +export function registerBankTypes(registry: Registry): void { + registry.register("/my.bank.v1beta1.TypeA", TypeA); + registry.register("/my.bank.v1beta1.TypeB", TypeB); +} + +// Register all staking-related types +export function registerStakingTypes(registry: Registry): void { + registry.register("/my.staking.v1beta1.TypeC", TypeC); + registry.register("/my.staking.v1beta1.TypeD", TypeD); +} +``` + +### 4. Include TypeScript Types + +Always define TypeScript interfaces for your custom types to get full type checking and IDE support: + +```typescript +// Good: With TypeScript interface +export interface MyType { + field1: string; + field2: number; +} + +// Avoid: Without TypeScript interface +export const MyType = { + typeUrl: "...", + encode: (message: any) => { /* ... */ }, + // ... +}; +``` + +### 5. Implement All Required Methods + +Make sure your custom types have all required methods: +- `encode` +- `decode` +- `fromPartial` +- (Optional) `fromJSON` and `toJSON` + +### 6. Test Your Custom Types + +Always test your custom types thoroughly to ensure they encode and decode correctly: + +```typescript +function testCustomType() { + // Create a message + const original = MsgTransferNFT.fromPartial({ + sender: "sender123", + recipient: "recipient456", + collectionId: "collection789", + tokenId: "token012" + }); + + // Encode to binary + const binary = MsgTransferNFT.encode(original).finish(); + + // Decode back + const decoded = MsgTransferNFT.decode(binary); + + // Verify fields match + console.assert(original.sender === decoded.sender, "Sender mismatch"); + console.assert(original.recipient === decoded.recipient, "Recipient mismatch"); + console.assert(original.collectionId === decoded.collectionId, "Collection ID mismatch"); + console.assert(original.tokenId === decoded.tokenId, "Token ID mismatch"); + + console.log("Test passed!"); +} +``` + +### 7. Register Both Proto and Amino Types + +If your application needs to work with wallets, make sure to register both Proto and Amino converters. + +## Conclusion + +Manual type registration allows you to extend Telescope's capabilities by integrating custom types with the generated code. This is essential when working with specialized modules, non-standard types, or extending chain functionality. + +By following the patterns and best practices outlined in this tutorial, you can seamlessly blend custom types with Telescope-generated code, creating a unified and type-safe experience for your Cosmos SDK application. + +In the next tutorial, we'll explore [JSON Patch Protos](./json-patch-protos.md) for modifying Protocol Buffer definitions without changing the original files. \ No newline at end of file diff --git a/learn/options.mdx b/learn/options.mdx new file mode 100644 index 0000000000..21b21704a5 --- /dev/null +++ b/learn/options.mdx @@ -0,0 +1,521 @@ +# Configuration Options Guide + +In this tutorial, we'll explore Telescope's configuration options and learn how to customize the generated TypeScript code for your project's specific needs. Telescope offers rich configuration options that give you precise control over every aspect of the code generation process. + +## Understanding the Structure of Configuration Options + +Telescope's configuration options are passed to the main function through a JavaScript object. The structure is as follows: + +```js +telescope({ + protoDirs, // Input directories + outPath, // Output directory + options: { + // All configuration options go here + } +}); +``` + +Configuration options can be divided into several main categories, each controlling different aspects of the generated code. Let's explore these categories one by one. + +## Amino Encoding Options + +Amino encoding is a serialization format used in Cosmos SDK that allows sharing data structures between different languages. These options are important when you need to maintain compatibility with older Cosmos SDK versions or work with wallets like Keplr. + +### Basic Setup + +The most basic setup is enabling or disabling Amino encoding: + +```js +options: { + aminoEncoding: { + enabled: true // Default is true + } +} +``` + +### Advanced Amino Configuration + +For more complex use cases, you can configure the following options: + +```js +options: { + aminoEncoding: { + enabled: true, + // Determines when to omit fields during JSON serialization + omitEmptyTags: ["omitempty", "dont_omitempty"], + // Disable generating AminoMsg types + disableMsgTypes: false, + // Set the casing function + casingFn: snake(), + // Set custom amino type names for specific message types + exceptions: { + '/cosmos.gov.v1beta1.MsgSubmitProposal': 'cosmos-sdk/MsgSubmitProposal' + } + } +} +``` + +### Practical Example + +Suppose you're building an application that needs to integrate with Keplr wallet, you might need to ensure amino types are correctly generated: + +```js +options: { + aminoEncoding: { + enabled: true, + // Use some project-specific amino naming conventions + exceptions: { + '/your.chain.module.MsgCustomAction': 'your-chain/MsgCustomAction' + } + } +} +``` + +## Interface and Implementation Options + +These options control how Telescope handles interfaces and their implementations. In Cosmos SDK, interfaces (like `sdk.Msg`) can have multiple concrete implementations, and these options help handle such cases. + +```js +options: { + interfaces: { + enabled: true, + // Enable global decoder registry, recommended when dealing with fields having 'accepted_interface' option + useGlobalDecoderRegistry: true, + // Use union types instead of intersection types for Any type + useUnionTypes: true + } +} +``` + +### When to Use Global Decoder Registry + +When working with modules that use interface fields (for example, Cosmos SDK's `Any` type), enabling `useGlobalDecoderRegistry` is very useful: + +```js +// This will allow proper decoding of messages with Any type fields +options: { + interfaces: { + enabled: true, + useGlobalDecoderRegistry: true, + useByDefault: true + } +} +``` + +## Prototype Options + +These options control how TypeScript types and methods are generated to handle Protocol Buffer messages. Prototype options are the core configuration, determining the structure and functionality of the generated code. + +### Basic Setup + +```js +options: { + prototypes: { + enabled: true, + // Export a protoPackage variable + includePackageVar: true, + // Whether fields are optional by default + fieldDefaultIsOptional: false + } +} +``` + +### Including and Excluding Specific Packages or Files + +If you only want to process specific packages or files, you can use include and exclude options: + +```js +options: { + prototypes: { + includes: { + packages: ["cosmos.bank", "cosmos.staking"], + protos: ["your/custom/proto/file.proto"] + }, + excluded: { + packages: ["unused.package"], + protos: ["unused/proto.proto"], + // Force exclude these files regardless of whether they are dependencies of other files + hardProtos: ["problematic/proto.proto"] + } + } +} +``` + +### Controlling Generated Methods + +You can precisely control which methods are generated: + +```js +options: { + prototypes: { + methods: { + encode: true, + decode: true, + fromJSON: true, + toJSON: true, + fromPartial: true, + // These methods are disabled by default + fromSDK: true, + toSDK: true + } + } +} +``` + +### Practical Example: Handling Specific Types + +Suppose you need special handling for timestamp and duration types: + +```js +options: { + prototypes: { + typingsFormat: { + // Use JavaScript Date object for Timestamp + timestamp: 'date', + // Use string for Duration + duration: 'string' + } + } +} +``` + +## Client Generation Options + +Telescope can generate different types of clients for interacting with the blockchain. These include LCD (REST) clients, RPC clients, and Stargate clients. + +### LCD Client Options + +LCD clients use HTTP/REST endpoints to query blockchain state: + +```js +options: { + lcdClients: { + enabled: true, + // Generate a factory bundle of all LCD clients + bundle: true, + // Create scoped client factories + scoped: [ + { + dir: 'mychain', + filename: 'custom-lcd-client.ts', + packages: ['cosmos.bank.v1beta1', 'mychain.module.v1'], + addToBundle: true, + methodName: 'createCustomLCDClient' + } + ] + } +} +``` + +### RPC Client Options + +RPC clients use Tendermint RPC, gRPC-web, or gRPC endpoints to interact with the blockchain: + +```js +options: { + rpcClients: { + enabled: true, + // Choose client type: 'tendermint', 'gRPC-web', or 'gRPC' + type: 'tendermint', + // Use camelCase for RPC methods + camelCase: true, + // Enabled service types + enabledServices: ['Msg', 'Query', 'Service'] + } +} +``` + +### Stargate Client Options + +Stargate clients are CosmJS-compatible clients used for signing and broadcasting transactions: + +```js +options: { + stargateClients: { + // Whether to include cosmjs default types + includeCosmosDefaultTypes: true, + // Add getSigningTxRpc to clients in namespaces + addGetTxRpc: true + } +} +``` + +### Practical Example: Creating Custom Clients + +Suppose you're building a dApp that needs to interact with specific modules: + +```js +options: { + rpcClients: { + enabled: true, + type: 'tendermint', + camelCase: true, + // Include only the services your application needs + scoped: [ + { + dir: 'mychain', + filename: 'my-rpc-client.ts', + packages: [ + 'cosmos.bank.v1beta1', + 'cosmos.staking.v1beta1', + 'mychain.module.v1' + ], + addToBundle: true, + methodNameQuery: 'createMyRPCQueryClient', + methodNameTx: 'createMyRPCTxClient' + } + ] + } +} +``` + +## Frontend Integration Options + +Telescope can integrate with various frontend frameworks and state management solutions like React Query, Mobx, Pinia, and Vue Query. + +### React Query Integration + +```js +options: { + reactQuery: { + enabled: true, + // Whether to allow extra query keys + needExtraQueryKey: true, + include: { + // Create hooks on matched packages + packages: ['cosmos.bank.v1beta1', 'cosmos.staking.v1beta1'] + }, + // Configure instant exports + instantExport: { + include: { + patterns: ['cosmos.bank.v1beta1.*'] + }, + // Custom naming mapping + nameMapping: { + useBankBalance: 'cosmos.bank.v1beta1.useBalance' + } + } + } +} +``` + +### State Management Integration (Mobx, Pinia) + +```js +options: { + // Mobx integration + mobx: { + enabled: true, + include: { + packages: ['cosmos.bank.v1beta1'] + } + }, + // Pinia integration (for Vue projects) + pinia: { + enabled: true, + include: { + packages: ['cosmos.staking.v1beta1'] + } + } +} +``` + +## Helper Functions Options + +Helper functions make common tasks easier, such as constructing messages or querying blockchain state: + +```js +options: { + helperFunctions: { + enabled: true, + // Generate React and Vue hooks + hooks: { + react: true, + vue: true + }, + include: { + // Service types to include + serviceTypes: ['Query', 'Msg'], + // Glob patterns to match specific services + patterns: ['cosmos.gov.v1beta1.**', 'cosmos.bank.v1beta1.*'] + }, + // Custom function naming + nameMappers: { + Query: { + funcBody: (name) => `get${name}`, + hookPrefix: 'use' + }, + Msg: { + funcBody: (name) => `send${name}`, + hookPrefix: 'useTx' + } + } + } +} +``` + +## Typing Format and Output Options + +These options control the format and style of the generated TypeScript code: + +```js +options: { + prototypes: { + typingsFormat: { + // Use DeepPartial type + useDeepPartial: true, + // Use Exact type + useExact: false, + // Use bigint for int64 + num64: 'bigint' + } + }, + // Remove unused imports + removeUnusedImports: true, + // Use arrow functions in classes + classesUseArrowFunctions: true +} +``` + +## Disabling TypeScript and ESLint Checks + +For some auto-generated files, you might want to disable TypeScript checking or ESLint: + +```js +options: { + tsDisable: { + // Disable TypeScript checking on all files + disableAll: false, + // Disable on matched patterns + patterns: ['**/amino/**'] + }, + eslintDisable: { + // Disable ESLint on matched files + files: ['**/tx.amino.ts'] + } +} +``` + +## Advanced Example: Complete Configuration + +Here's a more complete configuration example combining various options: + +```js +telescope({ + protoDirs: [join(__dirname, '/proto')], + outPath: join(__dirname, '/src/generated'), + options: { + aminoEncoding: { + enabled: true, + exceptions: { + '/cosmos.gov.v1beta1.MsgSubmitProposal': 'cosmos-sdk/MsgSubmitProposal' + } + }, + interfaces: { + enabled: true, + useGlobalDecoderRegistry: true + }, + prototypes: { + includePackageVar: true, + fieldDefaultIsOptional: true, + methods: { + encode: true, + decode: true, + fromJSON: true, + toJSON: true, + fromPartial: true + }, + typingsFormat: { + useDeepPartial: true, + timestamp: 'date', + duration: 'duration', + num64: 'bigint' + } + }, + lcdClients: { + enabled: true, + bundle: true + }, + rpcClients: { + enabled: true, + type: 'tendermint', + camelCase: true + }, + reactQuery: { + enabled: true, + include: { + packages: ['cosmos.bank.v1beta1', 'cosmos.staking.v1beta1'] + } + }, + helperFunctions: { + enabled: true, + hooks: { + react: true + }, + include: { + patterns: ['cosmos.bank.v1beta1.*'] + } + }, + tsDisable: { + patterns: ['**/amino/**'] + }, + removeUnusedImports: true + } +}); +``` + +## Choosing Configuration Based on Project Type + +Different types of projects may require different configurations. Here are some recommended configuration combinations: + +### Developing a Wallet or dApp Interface + +```js +options: { + aminoEncoding: { enabled: true }, + interfaces: { useGlobalDecoderRegistry: true }, + stargateClients: { enabled: true }, + reactQuery: { enabled: true } +} +``` + +### Developing a Blockchain Explorer + +```js +options: { + lcdClients: { enabled: true }, + rpcClients: { enabled: true, type: 'tendermint' }, + prototypes: { + typingsFormat: { + timestamp: 'date' + } + } +} +``` + +### Creating an On-chain Indexer or Analytics Tool + +```js +options: { + rpcClients: { enabled: true, type: 'gRPC' }, + prototypes: { + methods: { + fromSDK: true, + toSDK: true + } + } +} +``` + +## Summary + +Through this tutorial, you've learned about Telescope's main configuration options and how to customize them according to your project's requirements. Properly configuring these options can greatly enhance your development efficiency and generate TypeScript code that better meets your needs. + +In the next tutorial, we'll explore how to use the generated types and clients to build actual applications. + +## Further Learning + +- Check out the [Types documentation](./types.md) to learn how to use the generated types +- Explore the [Stargate Clients documentation](./stargate-clients.md) to learn how to use the generated clients +- Read the [Composing Messages documentation](./composing-messages.md) to learn how to create and send transactions \ No newline at end of file diff --git a/learn/quickstart.mdx b/learn/quickstart.mdx new file mode 100644 index 0000000000..c638a1f045 --- /dev/null +++ b/learn/quickstart.mdx @@ -0,0 +1,153 @@ +# Getting Started with Telescope + +Welcome to Telescope! This guide will walk you through the process of creating and publishing a TypeScript package for interacting with Cosmos SDK modules. + +## What is Telescope? + +Telescope is a tool that generates TypeScript client libraries from Cosmos SDK protocol buffer definitions. It allows you to build type-safe applications that interact with Cosmos blockchains. + +## Prerequisites + +Before we begin, make sure you have: +- Node.js (v14 or higher) +- npm or yarn +- Basic knowledge of TypeScript and the Cosmos SDK + +## Step 1: Install Required Tools + +First, let's install create-interchain-app globally: + +```sh +npm install -g create-interchain-app +``` + +This will give you access to the `cia` commands in your terminal. + +## Step 2: Generate a New Project + +We'll create a new project using the Telescope boilerplate: + +```sh +cia --boilerplate telescope +``` + +This command creates a new project with the necessary configuration files and folder structure. It will prompt you for a project name - enter something descriptive for your package. + +If you prefer to see this process in action, you can follow along with this video tutorial: https://youtu.be/iQf6p65fbdY + +## Step 3: Navigate to Your Package Directory + +Once the project is created, change to your module directory: + +```sh +cd ./your-project/packages/your-module +yarn install +``` + +This is where we'll be working for the rest of this tutorial. + +## Step 4: Download Protocol Buffers + +Now we need to download the protocol buffer definitions for the Cosmos SDK modules we want to work with: + +```sh +yarn download-protos +``` + +This command will: +1. Clone the specified repositories into `./git-modules` +2. Extract the proto files +3. Place them in the `./protos` directory + +Let's take a look at what's happening behind the scenes. When you run `yarn download-protos`, it executes a script located at `./scripts/download-protos.ts`. You can customize this script to download proto files from different repositories or specific branches. + +Here's an example of what that script might look like: + +```typescript +// ./scripts/download-protos.ts +import downloadProtos from '@cosmology/telescope/main/commands/download' + +const config = { + repos: [ + { owner: "cosmos", repo: "cosmos-sdk", branch: "release/v0.50.x" }, + { owner: "cosmos", repo: "ibc-go" }, + ], + protoDirMapping: { + "gogo/protobuf/master": ".", + "googleapis/googleapis/master": ".", + "protocolbuffers/protobuf/main": "src" + }, + outDir: "protos", + ssh: false, + tempRepoDir: "git-modules", + targets: [ + "cosmos/**/*.proto", + "ibc/**/*.proto", + ] +}; + +downloadProtos(config) + .then(() => console.log('✅ Proto download completed')) + // @ts-ignore + .catch((error) => { + console.error('❌ Proto download failed:', error); + process.exit(1); + }); +``` + +Feel free to modify this script to include additional repos or change branches as needed for your project. + +## Step 5: Generate TypeScript Code + +Now that we have our proto files, let's generate the TypeScript code: + +```sh +yarn codegen +``` + +This command runs the script at `./scripts/codegen.ts`, which uses Telescope to transpile the proto files into TypeScript. + +You can customize the code generation by modifying this script. For example, you might want to generate different types of clients or adjust the output formatting. + +The generated code will include: +- Protocol buffer message types +- gRPC service definitions +- REST/LCD client interfaces +- Helper methods for encoding/decoding data + +Take some time to explore the generated code to understand what's available to you. + +## Step 6: Build Your Package + +With the TypeScript code generated, we can now build the package: + +```sh +yarn build +``` + +This compiles the TypeScript files to JavaScript and generates type definitions, preparing your package for distribution. + +## Step 7: Publish Your Package + +Finally, you can publish your package to npm: + +```sh +npm publish +``` + +If you're using the create-interchain-app boilerplate with lerna for managing multiple packages, use: + +```sh +lerna publish +``` + +## Next Steps + +Congratulations! You've created a TypeScript package for interacting with Cosmos SDK modules. Here are some things you can do next: + +1. Create a sample application that uses your package +2. Add custom methods to extend the generated code +3. Implement specific business logic for your use case +4. Share your package with the Cosmos community + +In the following tutorials, we'll explore how to use your generated package in real-world applications. \ No newline at end of file diff --git a/learn/rpc-client-classes.mdx b/learn/rpc-client-classes.mdx new file mode 100644 index 0000000000..1be6207652 --- /dev/null +++ b/learn/rpc-client-classes.mdx @@ -0,0 +1,904 @@ +# Working with RPC Client Classes + +In this tutorial, we'll explore RPC Client Classes generated by Telescope, which provide an object-oriented approach to interacting with Cosmos SDK blockchains. These classes offer enhanced flexibility and extensibility compared to regular RPC clients. + +## Understanding RPC Client Classes + +RPC Client Classes take the functionality of standard RPC clients (covered in the [previous tutorial](./rpc-clients.md)) and wrap them in a class-based architecture. This approach offers several advantages: + +1. **Extensibility**: You can easily extend the classes to add custom methods +2. **Encapsulation**: Implementation details are hidden behind a clean interface +3. **Testability**: Classes are easier to mock and test +4. **Composition**: You can create composite clients by combining multiple module clients + +Let's dive in and see how to use these classes in your applications. + +## Setting Up Your Project + +First, let's enable RPC Client Classes generation in your Telescope configuration: + +```typescript +// In your Telescope configuration file +import { TelescopeOptions } from "@cosmology/types"; + +const options: TelescopeOptions = { + rpcClientClasses: { + enabled: true, + camelCase: true // Makes method names more JavaScript-friendly + } + // Other Telescope options... +}; + +export default options; +``` + +After running Telescope with this configuration, it will generate RPC Client Classes for all modules in your Protobuf definitions. + +## Creating Your First RPC Client Classes + +Let's start by creating a basic set of client classes to query the Cosmos Hub: + +```typescript +import { createRPCQueryClientClasses } from "./codegen/client-classes"; + +async function main() { + try { + // Create client classes + console.log("Creating RPC client classes..."); + const clients = createRPCQueryClientClasses({ + rpcEndpoint: "https://rpc.cosmos.network" + }); + + console.log("Client classes created successfully!"); + + // Access specific module clients + const bankClient = clients.cosmos.bank.v1beta1; + const stakingClient = clients.cosmos.staking.v1beta1; + const governanceClient = clients.cosmos.gov.v1beta1; + + // Use the bank client to query account balances + const address = "cosmos1..."; // Replace with a real address + const { balances } = await bankClient.allBalances({ address }); + + console.log(`Account ${address} has:`); + balances.forEach(coin => { + console.log(`- ${coin.amount} ${coin.denom}`); + }); + + // Use the staking client to query validators + const { validators } = await stakingClient.validators({ + status: "BOND_STATUS_BONDED" + }); + + console.log(`\nFound ${validators.length} active validators`); + console.log("Top 3 validators by voting power:"); + + validators + .sort((a, b) => parseInt(b.tokens) - parseInt(a.tokens)) + .slice(0, 3) + .forEach((validator, index) => { + console.log(`${index + 1}. ${validator.description.moniker}`); + console.log(` Voting power: ${parseInt(validator.tokens) / 1_000_000} ATOM`); + }); + + // Use the governance client to query proposals + const { proposals } = await governanceClient.proposals({}); + + console.log(`\nFound ${proposals.length} governance proposals`); + if (proposals.length > 0) { + console.log("Latest proposals:"); + proposals.slice(0, 3).forEach(proposal => { + console.log(`- #${proposal.proposalId}: ${proposal.content.title}`); + }); + } + } catch (error) { + console.error("Error:", error.message); + } +} + +main(); +``` + +## Exploring the Client Classes Structure + +Let's take a look at the structure of these client classes: + +```typescript +import { createRPCQueryClientClasses } from "./codegen/client-classes"; + +async function exploreClientStructure() { + const clients = createRPCQueryClientClasses({ + rpcEndpoint: "https://rpc.cosmos.network" + }); + + // The clients object has a nested structure that mirrors the protobuf packages + console.log("Client structure:"); + + // First level: namespace (e.g., cosmos, osmosis) + const namespaces = Object.keys(clients); + console.log("Available namespaces:", namespaces); + + // Second level: modules (e.g., bank, staking) + const cosmosModules = Object.keys(clients.cosmos); + console.log("\nCosmos modules:", cosmosModules); + + // Third level: versions (e.g., v1beta1) + const bankVersions = Object.keys(clients.cosmos.bank); + console.log("\nBank module versions:", bankVersions); + + // Fourth level: the actual client class instance + const bankClient = clients.cosmos.bank.v1beta1; + + // Examine methods available on the bank client + console.log("\nBank client methods:"); + console.log(Object.getOwnPropertyNames( + Object.getPrototypeOf(bankClient) + ).filter(name => name !== 'constructor')); + + // Log the type of the client + console.log("\nClient type:", bankClient.constructor.name); +} + +exploreClientStructure(); +``` + +## Understanding Different Client Types + +RPC Client Classes support different underlying RPC implementations. Let's examine each: + +```typescript +import { createRPCQueryClientClasses } from "./codegen/client-classes"; + +async function compareClientTypes() { + console.log("Creating clients with different implementations..."); + + // 1. Tendermint RPC Client (default) + const tendermintClients = createRPCQueryClientClasses({ + rpcEndpoint: "https://rpc.cosmos.network", + clientType: "tendermint" + }); + + // 2. gRPC-web Client (for browsers) + const grpcWebClients = createRPCQueryClientClasses({ + rpcEndpoint: "https://grpc-web.cosmos.network", + clientType: "grpc-web" + }); + + // 3. gRPC-gateway Client (REST-based) + const grpcGatewayClients = createRPCQueryClientClasses({ + rpcEndpoint: "https://rest.cosmos.network", + clientType: "grpc-gateway" + }); + + console.log("All client types created successfully!"); + + // Use each client type to perform the same query + const address = "cosmos1..."; // Replace with a real address + + try { + console.log("\nQuerying with Tendermint client:"); + const tendermintResult = await tendermintClients.cosmos.bank.v1beta1.balance({ + address, + denom: "uatom" + }); + console.log("Result:", tendermintResult.balance); + + console.log("\nQuerying with gRPC-web client:"); + const grpcWebResult = await grpcWebClients.cosmos.bank.v1beta1.balance({ + address, + denom: "uatom" + }); + console.log("Result:", grpcWebResult.balance); + + console.log("\nQuerying with gRPC-gateway client:"); + const grpcGatewayResult = await grpcGatewayClients.cosmos.bank.v1beta1.balance({ + address, + denom: "uatom" + }); + console.log("Result:", grpcGatewayResult.balance); + + console.log("\nAll client types work with the same interface!"); + } catch (error) { + console.error("Error during comparison:", error.message); + } +} + +compareClientTypes(); +``` + +## Extending Client Classes + +One of the main advantages of using client classes is the ability to extend them with custom functionality. Let's create an extended bank client: + +```typescript +import { createRPCQueryClientClasses } from "./codegen/client-classes"; +import { BankQueryClient } from "./codegen/cosmos/bank/v1beta1/query.rpc.Query"; + +// Create an extended bank client with additional methods +class EnhancedBankClient extends BankQueryClient { + // Add utility method to convert micro units to display units + convertToDisplayUnits(microAmount, decimals = 6) { + return Number(microAmount) / Math.pow(10, decimals); + } + + // Get formatted balance that's easy to read + async getFormattedBalance(address, denom) { + const { balance } = await this.balance({ + address, + denom + }); + + // If balance not found or zero + if (!balance || balance.amount === "0") { + return `0 ${this.getDenomName(denom)}`; + } + + // Convert amount based on denomination + const displayAmount = this.convertToDisplayUnits(balance.amount); + const displayDenom = this.getDenomName(denom); + + return `${displayAmount.toLocaleString()} ${displayDenom}`; + } + + // Get account summary with total value in specified denomination + async getAccountSummary(address) { + const { balances } = await this.allBalances({ + address + }); + + // Process each balance to add more information + const processedBalances = balances.map(coin => { + const displayAmount = this.convertToDisplayUnits(coin.amount); + const displayDenom = this.getDenomName(coin.denom); + + return { + denom: coin.denom, + displayDenom, + rawAmount: coin.amount, + displayAmount, + formatted: `${displayAmount.toLocaleString()} ${displayDenom}` + }; + }); + + // Calculate total (if there are tokens) + const total = processedBalances.reduce( + (sum, coin) => sum + coin.displayAmount, + 0 + ); + + return { + address, + balances: processedBalances, + totalBalances: processedBalances.length, + totalValue: total.toLocaleString(), + hasBalance: total > 0 + }; + } + + // Helper method to convert denomination to display name + getDenomName(denom) { + // Handle common denoms + if (denom === "uatom") return "ATOM"; + if (denom === "uosmo") return "OSMO"; + if (denom === "ujuno") return "JUNO"; + if (denom === "uluna") return "LUNA"; + + // For other micro denoms, remove the 'u' prefix and capitalize + if (denom.startsWith("u")) { + return denom.substring(1).toUpperCase(); + } + + // For IBC tokens, simplify + if (denom.startsWith("ibc/")) { + return "IBC Token"; + } + + // Default case: just return the original denom + return denom; + } +} + +// Usage example +async function useEnhancedClient() { + // Create the base client classes + const baseClients = await createRPCQueryClientClasses({ + rpcEndpoint: "https://rpc.cosmos.network" + }); + + // Extract the underlying clients needed for our enhanced client + const { rpc, queryClient } = baseClients.cosmos.bank.v1beta1; + + // Create our enhanced client + const enhancedBank = new EnhancedBankClient(rpc, queryClient); + + // Use our enhanced methods + const address = "cosmos1..."; // Replace with a real address + + try { + // Get formatted balance + const formattedAtom = await enhancedBank.getFormattedBalance(address, "uatom"); + console.log(`ATOM Balance: ${formattedAtom}`); + + // Get account summary + const summary = await enhancedBank.getAccountSummary(address); + + console.log("\nAccount Summary:"); + console.log(`Address: ${summary.address}`); + console.log(`Total Value: ${summary.totalValue}`); + console.log(`Number of tokens: ${summary.totalBalances}`); + + if (summary.balances.length > 0) { + console.log("\nToken Balances:"); + summary.balances.forEach(balance => { + console.log(`- ${balance.formatted}`); + }); + } else { + console.log("\nNo tokens found in this account."); + } + } catch (error) { + console.error("Error using enhanced client:", error.message); + } +} + +useEnhancedClient(); +``` + +## Creating a Composite Client + +For applications that work with multiple modules, you can create a composite client: + +```typescript +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; +import { QueryClient } from "@cosmjs/stargate"; +import { BankQueryClient } from "./codegen/cosmos/bank/v1beta1/query.rpc.Query"; +import { StakingQueryClient } from "./codegen/cosmos/staking/v1beta1/query.rpc.Query"; +import { DistributionQueryClient } from "./codegen/cosmos/distribution/v1beta1/query.rpc.Query"; + +// Create a composite client for account portfolio information +class AccountPortfolioClient { + private readonly bankClient: BankQueryClient; + private readonly stakingClient: StakingQueryClient; + private readonly distributionClient: DistributionQueryClient; + private readonly tendermintClient: Tendermint34Client; + + // Factory method to create and connect the client + static async connect(rpcEndpoint: string): Promise { + // Connect to the Tendermint RPC endpoint + const tendermintClient = await Tendermint34Client.connect(rpcEndpoint); + + // Create QueryClient that will be used by all module clients + const queryClient = new QueryClient(tendermintClient); + + // Create the individual module clients + const bankClient = new BankQueryClient(tendermintClient, queryClient); + const stakingClient = new StakingQueryClient(tendermintClient, queryClient); + const distributionClient = new DistributionQueryClient(tendermintClient, queryClient); + + // Return the composite client instance + return new AccountPortfolioClient( + tendermintClient, + bankClient, + stakingClient, + distributionClient + ); + } + + // Private constructor - use the static connect method to create instances + private constructor( + tendermintClient: Tendermint34Client, + bankClient: BankQueryClient, + stakingClient: StakingQueryClient, + distributionClient: DistributionQueryClient + ) { + this.tendermintClient = tendermintClient; + this.bankClient = bankClient; + this.stakingClient = stakingClient; + this.distributionClient = distributionClient; + } + + // Get complete portfolio with liquid, staked, and reward balances + async getFullPortfolio(address: string) { + // Execute all queries in parallel for better performance + const [ + balancesResponse, + delegationsResponse, + unbondingResponse, + rewardsResponse + ] = await Promise.all([ + this.bankClient.allBalances({ address }), + this.stakingClient.delegatorDelegations({ delegatorAddr: address }), + this.stakingClient.delegatorUnbondingDelegations({ delegatorAddr: address }), + this.distributionClient.delegationTotalRewards({ delegatorAddress: address }) + ]); + + // Process liquid balances + const liquidBalances = balancesResponse.balances.map(coin => ({ + denom: coin.denom, + amount: coin.amount, + displayAmount: this.formatAmount(coin.amount, coin.denom) + })); + + // Calculate total liquid balance in uatom (for simplicity) + const totalLiquid = liquidBalances.reduce( + (sum, coin) => coin.denom === "uatom" ? sum + Number(coin.amount) : sum, + 0 + ); + + // Process delegations (staked tokens) + const delegations = delegationsResponse.delegationResponses.map(del => ({ + validatorAddress: del.delegation.validatorAddress, + amount: del.balance.amount, + denom: del.balance.denom, + displayAmount: this.formatAmount(del.balance.amount, del.balance.denom) + })); + + // Calculate total staked + const totalStaked = delegations.reduce( + (sum, del) => del.denom === "uatom" ? sum + Number(del.amount) : sum, + 0 + ); + + // Process unbonding delegations + const unbonding = unbondingResponse.unbondingResponses.map(unbond => { + // Sum up all unbonding entries + const totalAmount = unbond.entries.reduce( + (sum, entry) => sum + Number(entry.balance), + 0 + ); + + return { + validatorAddress: unbond.validatorAddress, + entries: unbond.entries.length, + totalAmount: totalAmount.toString(), + displayAmount: this.formatAmount(totalAmount.toString(), "uatom") + }; + }); + + // Calculate total unbonding + const totalUnbonding = unbondingResponse.unbondingResponses.reduce( + (sum, unbond) => sum + unbond.entries.reduce( + (entrySum, entry) => entrySum + Number(entry.balance), + 0 + ), + 0 + ); + + // Process rewards + const rewards = rewardsResponse.rewards.map(reward => ({ + validatorAddress: reward.validatorAddress, + rewards: reward.reward.map(r => ({ + denom: r.denom, + amount: r.amount, + displayAmount: this.formatAmount(r.amount, r.denom) + })) + })); + + // Calculate total rewards (simplifying to just uatom rewards) + const totalRewards = rewardsResponse.total.reduce( + (sum, reward) => reward.denom === "uatom" ? sum + Number(reward.amount) : sum, + 0 + ); + + // Return comprehensive portfolio data + return { + address, + liquid: { + balances: liquidBalances, + total: totalLiquid, + displayTotal: this.formatAmount(totalLiquid.toString(), "uatom") + }, + staked: { + delegations, + total: totalStaked, + displayTotal: this.formatAmount(totalStaked.toString(), "uatom") + }, + unbonding: { + entries: unbonding, + total: totalUnbonding, + displayTotal: this.formatAmount(totalUnbonding.toString(), "uatom") + }, + rewards: { + details: rewards, + total: totalRewards, + displayTotal: this.formatAmount(totalRewards.toFixed(0), "uatom") + }, + // Calculate combined portfolio value + netWorth: { + total: totalLiquid + totalStaked + totalUnbonding + Math.floor(totalRewards), + displayTotal: this.formatAmount( + (totalLiquid + totalStaked + totalUnbonding + Math.floor(totalRewards)).toString(), + "uatom" + ) + } + }; + } + + // Helper to format token amounts for display + private formatAmount(amount: string, denom: string): string { + // Handle different denominations + if (denom === "uatom") { + const atoms = Number(amount) / 1_000_000; + return `${atoms.toLocaleString()} ATOM`; + } + + // For simplicity, just return the raw values for other denoms + return `${amount} ${denom}`; + } + + // Clean up resources + disconnect(): void { + this.tendermintClient.disconnect(); + } +} + +// Usage example +async function useCompositeClient() { + let client: AccountPortfolioClient | null = null; + + try { + // Create the composite client + client = await AccountPortfolioClient.connect("https://rpc.cosmos.network"); + + // Use the client to get portfolio information + const address = "cosmos1..."; // Replace with a real address + const portfolio = await client.getFullPortfolio(address); + + // Display the portfolio information + console.log(`=== Portfolio for ${address} ===`); + console.log(`\nLiquid balance: ${portfolio.liquid.displayTotal}`); + console.log(`Staked balance: ${portfolio.staked.displayTotal}`); + console.log(`Unbonding: ${portfolio.unbonding.displayTotal}`); + console.log(`Pending rewards: ${portfolio.rewards.displayTotal}`); + console.log(`\nTotal net worth: ${portfolio.netWorth.displayTotal}`); + + // Show delegations + if (portfolio.staked.delegations.length > 0) { + console.log("\nDelegations:"); + portfolio.staked.delegations.forEach(del => { + console.log(`- Validator ${del.validatorAddress}: ${del.displayAmount}`); + }); + } + + // Show liquid balances + if (portfolio.liquid.balances.length > 0) { + console.log("\nLiquid balances:"); + portfolio.liquid.balances.forEach(balance => { + console.log(`- ${balance.displayAmount}`); + }); + } + } catch (error) { + console.error("Error:", error.message); + } finally { + // Always clean up resources + if (client) { + client.disconnect(); + } + } +} + +useCompositeClient(); +``` + +## Working with Tendermint Subscriptions + +One of the powerful features of the Tendermint RPC client is the ability to subscribe to real-time events. Let's see how to combine this with our client classes: + +```typescript +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; +import { createRPCQueryClientClasses } from "./codegen/client-classes"; + +async function monitorBlocksAndValidators() { + let tmClient: Tendermint34Client | null = null; + + try { + console.log("Connecting to Tendermint WebSocket..."); + tmClient = await Tendermint34Client.connect("wss://rpc.cosmos.network/websocket"); + console.log("Connected!"); + + // Create RPC Client Classes using our existing Tendermint client + const clientClasses = createRPCQueryClientClasses({ + rpcEndpoint: tmClient + }); + + // Get initial validator set + const { validators: initialValidators } = await clientClasses.cosmos.staking.v1beta1.validators({ + status: "BOND_STATUS_BONDED" + }); + + console.log(`Initial validator count: ${initialValidators.length}`); + + // Create a validator map for quick lookups + const validatorMap = new Map(); + initialValidators.forEach(validator => { + validatorMap.set(validator.operatorAddress, { + moniker: validator.description.moniker, + votingPower: validator.tokens + }); + }); + + // Subscribe to new blocks + console.log("Subscribing to new blocks..."); + const subscription = tmClient.subscribeNewBlock().subscribe({ + next: async (block) => { + const height = block.header.height; + const time = new Date(block.header.time).toLocaleString(); + const txCount = block.txs.length; + + console.log(`\nNew block #${height} at ${time}`); + console.log(`Contains ${txCount} transactions`); + + // If this is a significant block (every 100 blocks), query validators again + if (parseInt(height) % 100 === 0) { + try { + console.log("Checking for validator changes..."); + + const { validators: currentValidators } = await clientClasses.cosmos.staking.v1beta1.validators({ + status: "BOND_STATUS_BONDED" + }); + + // Check for changes in the validator set + let changes = false; + + // Check for new validators + currentValidators.forEach(validator => { + if (!validatorMap.has(validator.operatorAddress)) { + console.log(`New validator: ${validator.description.moniker}`); + changes = true; + + // Update our map + validatorMap.set(validator.operatorAddress, { + moniker: validator.description.moniker, + votingPower: validator.tokens + }); + } else { + // Check for voting power changes + const oldPower = validatorMap.get(validator.operatorAddress).votingPower; + if (oldPower !== validator.tokens) { + const oldPowerNum = parseInt(oldPower); + const newPowerNum = parseInt(validator.tokens); + const percentChange = ((newPowerNum - oldPowerNum) / oldPowerNum * 100).toFixed(2); + + console.log(`Validator ${validator.description.moniker} voting power changed by ${percentChange}%`); + changes = true; + + // Update our map + validatorMap.set(validator.operatorAddress, { + moniker: validator.description.moniker, + votingPower: validator.tokens + }); + } + } + }); + + // Check for removed validators + const currentValidatorAddresses = new Set( + currentValidators.map(v => v.operatorAddress) + ); + + validatorMap.forEach((value, address) => { + if (!currentValidatorAddresses.has(address)) { + console.log(`Validator removed: ${value.moniker}`); + changes = true; + + // Remove from our map + validatorMap.delete(address); + } + }); + + if (!changes) { + console.log("No validator changes detected"); + } + } catch (error) { + console.error("Error checking validators:", error.message); + } + } + }, + error: (err) => { + console.error("Subscription error:", err.message); + } + }); + + // Keep the subscription active for demonstration + console.log("Monitoring blocks. Press Ctrl+C to exit..."); + + // For demonstration purposes, keep the process running + await new Promise(resolve => setTimeout(resolve, 300000)); // 5 minutes + + // Clean up + subscription.unsubscribe(); + + } catch (error) { + console.error("Error:", error.message); + } finally { + // Always clean up resources + if (tmClient) { + console.log("Disconnecting..."); + tmClient.disconnect(); + } + } +} + +monitorBlocksAndValidators(); +``` + +## Implementing Pagination Helpers + +Many queries return large result sets that require pagination. Let's create a client with pagination helpers: + +```typescript +import { StakingQueryClient } from "./codegen/cosmos/staking/v1beta1/query.rpc.Query"; +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; +import { QueryClient } from "@cosmjs/stargate"; + +class PaginatedStakingClient extends StakingQueryClient { + // Get all validators with automatic pagination + async getAllValidators(status = "") { + let allValidators = []; + let nextKey = null; + let pageCount = 0; + + console.log("Fetching all validators with pagination..."); + + do { + pageCount++; + + // Prepare request with pagination + const request: any = {}; + + // Add status filter if provided + if (status) { + request.status = status; + } + + // Add pagination parameters + request.pagination = nextKey + ? { key: nextKey, limit: "100" } + : { limit: "100" }; + + // Query this page of validators + const response = await this.validators(request); + + // Process this page + const validators = response.validators; + console.log(`Page ${pageCount}: Retrieved ${validators.length} validators`); + + // Add to our collection + allValidators = [...allValidators, ...validators]; + + // Get key for next page (if any) + nextKey = response.pagination?.nextKey || null; + + } while (nextKey && nextKey.length > 0); + + console.log(`Completed pagination. Retrieved ${allValidators.length} validators in total`); + return allValidators; + } + + // Get all delegations for a delegator with automatic pagination + async getAllDelegatorDelegations(delegatorAddr) { + let allDelegations = []; + let nextKey = null; + let pageCount = 0; + + console.log(`Fetching all delegations for ${delegatorAddr}...`); + + do { + pageCount++; + + // Prepare request with pagination + const request = { + delegatorAddr, + pagination: nextKey + ? { key: nextKey, limit: "100" } + : { limit: "100" } + }; + + // Query this page of delegations + const response = await this.delegatorDelegations(request); + + // Process this page + const delegations = response.delegationResponses; + console.log(`Page ${pageCount}: Retrieved ${delegations.length} delegations`); + + // Add to our collection + allDelegations = [...allDelegations, ...delegations]; + + // Get key for next page (if any) + nextKey = response.pagination?.nextKey || null; + + } while (nextKey && nextKey.length > 0); + + console.log(`Completed pagination. Retrieved ${allDelegations.length} delegations in total`); + return allDelegations; + } +} + +// Usage example +async function usePaginationHelpers() { + let tmClient: Tendermint34Client | null = null; + + try { + // Connect to the blockchain + tmClient = await Tendermint34Client.connect("https://rpc.cosmos.network"); + const queryClient = new QueryClient(tmClient); + + // Create our enhanced client + const stakingClient = new PaginatedStakingClient(tmClient, queryClient); + + // Get all active validators + console.log("Getting all bonded validators..."); + const bondedValidators = await stakingClient.getAllValidators("BOND_STATUS_BONDED"); + console.log(`Found ${bondedValidators.length} active validators`); + + // Get all validators (including unbonded and unbonding) + console.log("\nGetting all validators..."); + const allValidators = await stakingClient.getAllValidators(); + console.log(`Found ${allValidators.length} total validators`); + + // Analyze validator statuses + const statusCounts = { + "BOND_STATUS_BONDED": 0, + "BOND_STATUS_UNBONDING": 0, + "BOND_STATUS_UNBONDED": 0, + "BOND_STATUS_UNSPECIFIED": 0 + }; + + allValidators.forEach(validator => { + statusCounts[validator.status]++; + }); + + console.log("\nValidator status breakdown:"); + console.log(`- Bonded: ${statusCounts["BOND_STATUS_BONDED"]}`); + console.log(`- Unbonding: ${statusCounts["BOND_STATUS_UNBONDING"]}`); + console.log(`- Unbonded: ${statusCounts["BOND_STATUS_UNBONDED"]}`); + console.log(`- Unspecified: ${statusCounts["BOND_STATUS_UNSPECIFIED"]}`); + + // Now get all delegations for an address + const address = "cosmos1..."; // Replace with a real address + console.log(`\nGetting all delegations for ${address}...`); + + const delegations = await stakingClient.getAllDelegatorDelegations(address); + console.log(`Found ${delegations.length} delegations`); + + if (delegations.length > 0) { + console.log("\nDelegation summary:"); + let totalStaked = 0; + + delegations.forEach(del => { + const amount = parseInt(del.balance.amount); + totalStaked += amount; + + console.log(`- ${del.delegation.validatorAddress}: ${amount / 1_000_000} ATOM`); + }); + + console.log(`\nTotal staked: ${totalStaked / 1_000_000} ATOM`); + } + + } catch (error) { + console.error("Error:", error.message); + } finally { + // Always clean up + if (tmClient) { + tmClient.disconnect(); + } + } +} + +usePaginationHelpers(); +``` + +## Conclusion + +In this tutorial, you've learned how to: + +1. Enable and generate RPC Client Classes with Telescope +2. Create and use basic RPC Client Classes +3. Understand the different client types (Tendermint, gRPC-web, gRPC-gateway) +4. Extend client classes with custom functionality +5. Create composite clients that combine multiple module clients +6. Work with Tendermint subscriptions for real-time updates +7. Implement pagination helpers for large data sets + +RPC Client Classes provide a powerful object-oriented approach to interacting with Cosmos SDK blockchains. By extending and combining these classes, you can create reusable, maintainable code that fits your application's specific needs. + +In the next tutorial, we'll explore [Instant RPC Methods](./instant-rpc-methods.md) for simplified access to common blockchain operations. \ No newline at end of file diff --git a/learn/rpc-clients.mdx b/learn/rpc-clients.mdx new file mode 100644 index 0000000000..e97b93d999 --- /dev/null +++ b/learn/rpc-clients.mdx @@ -0,0 +1,608 @@ +# Understanding and Using RPC Clients + +In this tutorial, we'll explore how to use RPC (Remote Procedure Call) clients generated by Telescope to interact with Cosmos SDK blockchains. RPC clients provide powerful and direct access to blockchain nodes, allowing your applications to query data and subscribe to real-time events. + +## What Are RPC Clients? + +RPC clients allow your application to make direct calls to blockchain nodes, giving you access to the full range of functionality provided by Cosmos SDK chains. Telescope generates type-safe TypeScript clients that make these interactions simpler and less error-prone. + +There are three main types of RPC clients that Telescope can generate: + +1. **Tendermint RPC Client** - The most direct and powerful interface to Tendermint nodes +2. **gRPC-web Client** - Browser-compatible gRPC client +3. **gRPC-gateway Client** - REST-based access to gRPC services + +In this tutorial, we'll learn how to use each of these client types and understand their strengths and best use cases. + +## Setting Up Your Project + +First, let's enable RPC client generation in your Telescope configuration: + +```typescript +// In your Telescope configuration file +import { TelescopeOptions } from "@cosmology/types"; + +const options: TelescopeOptions = { + rpcClients: { + enabled: true, + camelCase: true // Makes method names more JavaScript-friendly + } + // Other Telescope options... +}; + +export default options; +``` + +After running Telescope with this configuration, it will generate RPC client code for all the modules in your Protobuf definitions. + +## Creating Your First RPC Client + +Let's start by creating a basic RPC client to connect to the Cosmos Hub: + +```typescript +import { createRPCQueryClient } from "./codegen/rpc"; + +async function main() { + try { + // Create an RPC client + console.log("Connecting to Cosmos Hub..."); + const client = await createRPCQueryClient({ + rpcEndpoint: "https://rpc.cosmos.network" + }); + + console.log("Connected successfully!"); + + // Let's get some basic information about the chain + const nodeInfo = await client.cosmos.base.tendermint.v1beta1.getNodeInfo({}); + console.log("Connected to chain:", nodeInfo.defaultNodeInfo.network); + console.log("Node version:", nodeInfo.applicationVersion.version); + + // Query the latest block + const latestBlock = await client.cosmos.base.tendermint.v1beta1.getLatestBlock({}); + console.log("Latest block height:", latestBlock.block.header.height); + console.log("Latest block time:", new Date(latestBlock.block.header.time).toLocaleString()); + + // Get some information about validators + const { validators } = await client.cosmos.staking.v1beta1.validators({ + status: "BOND_STATUS_BONDED" + }); + + console.log(`Found ${validators.length} active validators`); + + // Show the top 5 validators by voting power + console.log("\nTop validators:"); + validators + .sort((a, b) => parseInt(b.tokens) - parseInt(a.tokens)) + .slice(0, 5) + .forEach((validator, index) => { + console.log(`${index + 1}. ${validator.description.moniker}`); + console.log(` Voting power: ${parseInt(validator.tokens) / 1_000_000} ATOM`); + }); + } catch (error) { + console.error("Error connecting to the blockchain:", error.message); + } +} + +main(); +``` + +## Understanding the Tendermint RPC Client + +The Tendermint RPC client provides the most direct access to Tendermint nodes. It's great for server-side applications that need high-performance and access to real-time events. + +```typescript +import { createRPCQueryClient } from "./codegen/rpc"; +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; + +async function exploreTemdermintRPC() { + try { + // Create a client with explicit client type + const client = await createRPCQueryClient({ + rpcEndpoint: "https://rpc.cosmos.network", + clientType: "tendermint" // Explicitly specify Tendermint client + }); + + // You can also create a raw Tendermint client for advanced use cases + const tmClient = await Tendermint34Client.connect("https://rpc.cosmos.network"); + + // Get the status of the node + const status = await tmClient.status(); + console.log("Node status:"); + console.log("- Chain ID:", status.nodeInfo.network); + console.log("- Latest block:", status.syncInfo.latestBlockHeight); + console.log("- Catching up:", status.syncInfo.catchingUp); + + // Get a specific block + const blockHeight = 9000000; // Choose a block height + const block = await tmClient.block(blockHeight); + + console.log(`\nBlock #${blockHeight}:`); + console.log("- Time:", new Date(block.block.header.time).toLocaleString()); + console.log("- Proposer:", block.block.header.proposerAddress); + console.log("- Number of transactions:", block.block.txs.length); + + // Get network information + const netInfo = await tmClient.netInfo(); + console.log("\nNetwork information:"); + console.log("- Number of peers:", netInfo.nPeers); + console.log("- Is listening:", netInfo.listening); + + // Always disconnect when done + tmClient.disconnect(); + } catch (error) { + console.error("Error exploring Tendermint RPC:", error.message); + } +} +``` + +## Working with gRPC-web Client + +For browser applications, the gRPC-web client provides a compatible interface: + +```typescript +import { createRPCQueryClient } from "./codegen/rpc"; + +async function browserCompatibleQueries() { + try { + // Create a gRPC-web client for browser compatibility + const client = await createRPCQueryClient({ + rpcEndpoint: "https://grpc-web.cosmos.network", + clientType: "grpc-web" + }); + + // Query account balances + const address = "cosmos1..."; // Replace with a real address + const { balances } = await client.cosmos.bank.v1beta1.allBalances({ + address + }); + + console.log(`Account ${address} balances:`); + if (balances.length === 0) { + console.log("No tokens found"); + } else { + balances.forEach(coin => { + console.log(`${coin.amount} ${coin.denom}`); + }); + } + + // Query governance proposals + const { proposals } = await client.cosmos.gov.v1beta1.proposals({}); + + console.log(`\nFound ${proposals.length} governance proposals:`); + proposals.forEach(proposal => { + console.log(`- #${proposal.proposalId}: ${proposal.content.title}`); + console.log(` Status: ${getProposalStatus(proposal.status)}`); + console.log(` Voting ends: ${new Date(proposal.votingEndTime).toLocaleString()}`); + }); + } catch (error) { + console.error("Error in browser-compatible queries:", error.message); + } +} + +// Helper function to convert proposal status to readable form +function getProposalStatus(status) { + const statuses = [ + "UNSPECIFIED", + "DEPOSIT_PERIOD", + "VOTING_PERIOD", + "PASSED", + "REJECTED", + "FAILED" + ]; + return statuses[status] || "UNKNOWN"; +} +``` + +## Using gRPC-gateway Client + +The gRPC-gateway client provides a REST-based interface to gRPC services: + +```typescript +import { createRPCQueryClient } from "./codegen/rpc"; + +async function restBasedQueries() { + try { + // Create a gRPC-gateway client (REST-based) + const client = await createRPCQueryClient({ + rpcEndpoint: "https://rest.cosmos.network", + clientType: "grpc-gateway" + }); + + // Query inflation rate + const inflation = await client.cosmos.mint.v1beta1.inflation({}); + const inflationRate = parseFloat(inflation.inflation) * 100; + console.log(`Current inflation rate: ${inflationRate.toFixed(2)}%`); + + // Query total supply + const { supply } = await client.cosmos.bank.v1beta1.totalSupply({}); + + console.log("\nTotal supply:"); + supply.forEach(coin => { + if (coin.denom === "uatom") { + const atomAmount = parseInt(coin.amount) / 1_000_000; + console.log(`${atomAmount.toLocaleString()} ATOM`); + } else { + console.log(`${coin.amount} ${coin.denom}`); + } + }); + } catch (error) { + console.error("Error in REST-based queries:", error.message); + } +} +``` + +## Handling Pagination + +Many RPC methods return large datasets that are paginated. Here's how to work with pagination: + +```typescript +import { createRPCQueryClient } from "./codegen/rpc"; + +async function handlePagination() { + try { + const client = await createRPCQueryClient({ + rpcEndpoint: "https://rpc.cosmos.network" + }); + + // Example 1: Using offset-based pagination + console.log("Using offset-based pagination:"); + let pageNumber = 1; + const pageSize = 10; + + const firstPage = await client.cosmos.staking.v1beta1.validators({ + pagination: { + offset: "0", + limit: pageSize.toString(), + countTotal: true + } + }); + + console.log(`Page ${pageNumber}: ${firstPage.validators.length} validators`); + console.log(`Total validators: ${firstPage.pagination.total}`); + + // Fetch second page + pageNumber++; + const secondPage = await client.cosmos.staking.v1beta1.validators({ + pagination: { + offset: pageSize.toString(), + limit: pageSize.toString() + } + }); + + console.log(`Page ${pageNumber}: ${secondPage.validators.length} validators`); + + // Example 2: Using key-based pagination (more efficient for large datasets) + console.log("\nUsing key-based pagination:"); + + // Function to get all validators using key-based pagination + async function getAllValidators() { + let allValidators = []; + let nextKey = null; + let pageCount = 0; + + do { + pageCount++; + // Set up pagination parameters + const paginationParams = nextKey + ? { key: nextKey, limit: "50" } + : { limit: "50" }; + + // Make the query + const response = await client.cosmos.staking.v1beta1.validators({ + pagination: paginationParams + }); + + // Add this page of validators to our result + allValidators = [...allValidators, ...response.validators]; + + // Get the key for the next page + nextKey = response.pagination?.nextKey || null; + + console.log(`Page ${pageCount}: fetched ${response.validators.length} validators, running total: ${allValidators.length}`); + + } while (nextKey && nextKey.length > 0); + + return allValidators; + } + + const allValidators = await getAllValidators(); + console.log(`Retrieved all ${allValidators.length} validators`); + } catch (error) { + console.error("Error handling pagination:", error.message); + } +} +``` + +## Subscribing to Real-time Events + +One of the powerful features of Tendermint RPC is the ability to subscribe to real-time events. Let's see how to subscribe to new blocks: + +```typescript +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; + +async function subscribeToEvents() { + let tmClient; + try { + console.log("Connecting to Tendermint WebSocket..."); + tmClient = await Tendermint34Client.connect("wss://rpc.cosmos.network/websocket"); + console.log("Connected! Subscribing to events..."); + + // Subscribe to new blocks + console.log("Subscribing to new blocks..."); + const blockSubscription = tmClient.subscribeNewBlock().subscribe({ + next: (block) => { + console.log(`New block #${block.header.height} at ${new Date(block.header.time).toLocaleString()}`); + console.log(`Contains ${block.txs.length} transactions`); + }, + error: (err) => { + console.error("Block subscription error:", err.message); + }, + complete: () => { + console.log("Block subscription completed"); + } + }); + + // Subscribe to transactions + console.log("Subscribing to transactions..."); + const txSubscription = tmClient.subscribeTx().subscribe({ + next: (txEvent) => { + console.log(`New transaction: ${Buffer.from(txEvent.hash).toString('hex')}`); + console.log(`Result code: ${txEvent.result.code} (${txEvent.result.code === 0 ? "success" : "failed"})`); + }, + error: (err) => { + console.error("Transaction subscription error:", err.message); + } + }); + + // Keep the subscription active for a minute + console.log("Watching for events for 60 seconds..."); + await new Promise(resolve => setTimeout(resolve, 60000)); + + // Unsubscribe when done + console.log("Unsubscribing..."); + blockSubscription.unsubscribe(); + txSubscription.unsubscribe(); + + } catch (error) { + console.error("Error in event subscription:", error.message); + } finally { + // Always disconnect the client + if (tmClient) { + console.log("Disconnecting..."); + tmClient.disconnect(); + } + } +} +``` + +## Error Handling and Retries + +In production applications, you'll want to handle errors gracefully and implement retries for transient failures: + +```typescript +import { createRPCQueryClient } from "./codegen/rpc"; + +async function queryWithRetries(endpoint, queryFn, maxRetries = 3) { + let client; + try { + client = await createRPCQueryClient({ + rpcEndpoint: endpoint + }); + + let attempt = 0; + while (attempt < maxRetries) { + try { + attempt++; + console.log(`Attempt ${attempt}/${maxRetries}...`); + + // Execute the provided query function with the client + const result = await queryFn(client); + return result; // Success! + } catch (error) { + if (attempt >= maxRetries) { + console.error(`All ${maxRetries} attempts failed`); + throw error; // Re-throw after all attempts + } + + // Determine if we should retry based on the error + if (isRetriableError(error)) { + const delay = calculateBackoff(attempt); + console.warn(`Retriable error: ${error.message}. Retrying in ${delay}ms...`); + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + console.error(`Non-retriable error: ${error.message}`); + throw error; // Don't retry non-retriable errors + } + } + } + } finally { + // Clean up resources if needed + } +} + +// Helper function to determine if an error is retriable +function isRetriableError(error) { + // Network errors or server errors (5xx) are generally retriable + return ( + !error.response || // Network error + error.code === 4 || // DEADLINE_EXCEEDED + error.code === 14 || // UNAVAILABLE + (error.response && error.response.status >= 500) // Server error + ); +} + +// Helper function for exponential backoff +function calculateBackoff(attempt) { + const baseDelay = 1000; // 1 second + return baseDelay * Math.pow(2, attempt - 1); // 1s, 2s, 4s, 8s, etc. +} + +// Example usage +async function performResilientQuery() { + try { + // Define the query we want to execute with retry logic + const result = await queryWithRetries( + "https://rpc.cosmos.network", + async (client) => { + const { validators } = await client.cosmos.staking.v1beta1.validators({ + status: "BOND_STATUS_BONDED" + }); + return validators; + }, + 3 // Maximum 3 retries + ); + + console.log(`Successfully queried ${result.length} validators`); + } catch (error) { + console.error("Query ultimately failed:", error.message); + } +} +``` + +## Building a Chain Explorer Dashboard + +Let's put everything together and build a simple blockchain explorer dashboard: + +```typescript +import { createRPCQueryClient } from "./codegen/rpc"; + +async function createBlockchainDashboard() { + try { + console.log("Connecting to Cosmos Hub..."); + const client = await createRPCQueryClient({ + rpcEndpoint: "https://rpc.cosmos.network" + }); + + // Fetch multiple pieces of data in parallel for efficiency + const [ + nodeInfo, + latestBlock, + validatorsResponse, + stakingParams, + inflation, + supplyResponse, + govProposals + ] = await Promise.all([ + client.cosmos.base.tendermint.v1beta1.getNodeInfo({}), + client.cosmos.base.tendermint.v1beta1.getLatestBlock({}), + client.cosmos.staking.v1beta1.validators({ status: "BOND_STATUS_BONDED" }), + client.cosmos.staking.v1beta1.params({}), + client.cosmos.mint.v1beta1.inflation({}), + client.cosmos.bank.v1beta1.totalSupply({}), + client.cosmos.gov.v1beta1.proposals({ proposalStatus: 2 }) // Voting period + ]); + + // Extract and process the data + const chainId = nodeInfo.defaultNodeInfo.network; + const nodeVersion = nodeInfo.applicationVersion.version; + const blockHeight = latestBlock.block.header.height; + const blockTime = new Date(latestBlock.block.header.time); + + const validators = validatorsResponse.validators; + const bondedTokens = stakingParams.params.bondDenom; + + // Calculate inflation + const inflationRate = parseFloat(inflation.inflation) * 100; + + // Get ATOM supply + const atomSupply = supplyResponse.supply.find(coin => coin.denom === "uatom"); + const totalAtoms = atomSupply + ? parseInt(atomSupply.amount) / 1_000_000 + : 0; + + // Get active proposals + const activeProposals = govProposals.proposals; + + // Display the dashboard + console.log(`\n===== ${chainId} Blockchain Dashboard =====`); + console.log(`Current Time: ${new Date().toLocaleString()}`); + console.log("\n--- Network Information ---"); + console.log(`Node Version: ${nodeVersion}`); + console.log(`Latest Block: ${blockHeight}`); + console.log(`Block Time: ${blockTime.toLocaleString()}`); + console.log(`Time Since Last Block: ${Math.round((Date.now() - blockTime.getTime()) / 1000)} seconds`); + + console.log("\n--- Validator Information ---"); + console.log(`Active Validators: ${validators.length}`); + console.log(`Bonded Token Denom: ${bondedTokens}`); + + console.log("\nTop 5 Validators by Voting Power:"); + validators + .sort((a, b) => parseInt(b.tokens) - parseInt(a.tokens)) + .slice(0, 5) + .forEach((v, i) => { + const votingPower = parseInt(v.tokens) / 1_000_000; + const votingPercent = (votingPower / totalAtoms * 100).toFixed(2); + console.log(`${i+1}. ${v.description.moniker} - ${votingPower.toLocaleString()} ATOM (${votingPercent}%)`); + }); + + console.log("\n--- Economic Information ---"); + console.log(`Total Supply: ${totalAtoms.toLocaleString()} ATOM`); + console.log(`Inflation Rate: ${inflationRate.toFixed(2)}%`); + + console.log("\n--- Governance Information ---"); + console.log(`Active Proposals: ${activeProposals.length}`); + + if (activeProposals.length > 0) { + console.log("\nProposals in Voting Period:"); + activeProposals.forEach(p => { + console.log(`- Proposal #${p.proposalId}: ${p.content.title}`); + console.log(` Voting Ends: ${new Date(p.votingEndTime).toLocaleString()}`); + }); + } + + console.log("\n========================================"); + } catch (error) { + console.error("Error creating dashboard:", error.message); + } +} + +createBlockchainDashboard(); +``` + +## Choosing the Right Client Type + +Here's a guide to help you choose the right client type for your application: + +1. **Tendermint RPC Client** + - Best for: Server-side applications, applications that need real-time data via subscriptions + - Pros: Most direct and efficient, supports WebSocket subscriptions + - Cons: Limited browser compatibility (WebSocket only) + +2. **gRPC-web Client** + - Best for: Browser applications that need efficient querying + - Pros: Works in browsers, more efficient than REST + - Cons: Limited subscription support, requires special proxy in production + +3. **gRPC-gateway Client** + - Best for: Simple browser applications, when compatibility is most important + - Pros: Works everywhere, standard REST interface + - Cons: Less efficient, no subscription support + +## Best Practices + +Here are some best practices to follow when using RPC clients: + +1. **Manage Client Lifecycle**: Always create and dispose of clients properly +2. **Use Pagination**: For large datasets, implement proper pagination +3. **Batch Requests**: Use Promise.all to execute multiple queries in parallel +4. **Handle Errors**: Implement proper error handling with retries for transient failures +5. **Rate Limiting**: Be mindful of rate limits on public nodes +6. **Connection Pooling**: Reuse clients instead of creating new ones for each request +7. **Timeout Management**: Set appropriate timeouts for different operations + +## Conclusion + +In this tutorial, you've learned how to: + +1. Configure Telescope to generate RPC clients +2. Create and use different types of RPC clients +3. Query blockchain data with type safety +4. Handle pagination for large datasets +5. Subscribe to real-time blockchain events +6. Implement error handling and retries +7. Build a complete blockchain explorer dashboard + +RPC clients provide powerful and direct access to blockchain nodes. By using Telescope-generated clients, you get the benefits of this power combined with the safety and convenience of TypeScript's type system. + +In the next tutorial, we'll explore [RPC Client Classes](./rpc-client-classes.md) for more advanced and object-oriented usage patterns. \ No newline at end of file diff --git a/learn/sponsors.mdx b/learn/sponsors.mdx new file mode 100644 index 0000000000..af4abdd72c --- /dev/null +++ b/learn/sponsors.mdx @@ -0,0 +1,216 @@ +# Supporting the Telescope Project + +In this tutorial, we'll explore how to support the Telescope project as a sponsor and understand how your contribution helps advance the development of TypeScript tooling for the Cosmos ecosystem. + +## Why Sponsor Telescope? + +Telescope plays a crucial role in the Cosmos ecosystem by making blockchain development more accessible to TypeScript and JavaScript developers. By sponsoring Telescope, you contribute to: + +1. **Ongoing maintenance and improvements** - Ensuring the tool remains reliable and up-to-date +2. **New feature development** - Expanding capabilities to meet evolving developer needs +3. **Better documentation and examples** - Making it easier for new developers to join the ecosystem +4. **Community support** - Helping users solve problems and overcome challenges + +Let's explore how individuals and organizations can support this important project. + +## Understanding Telescope's Funding Model + +Telescope follows an open-source funding model that combines: + +- Corporate sponsorships +- Individual donations +- Grants from ecosystem foundations +- Voluntary contributions + +This diverse funding approach ensures that Telescope remains community-focused while having the resources needed for sustainable development. + +## How to Sponsor as an Individual Developer + +If you're an individual developer who benefits from Telescope, here's how you can contribute: + +### Step 1: Choose Your Sponsorship Platform + +Telescope accepts individual sponsorships through GitHub Sponsors, which provides a simple monthly contribution system: + +1. Visit the [Telescope GitHub Sponsors page](https://github.com/sponsors/hyperweb-io) +2. Select a monthly contribution amount that feels comfortable for you +3. Complete the GitHub Sponsors form with your payment information + +Even small contributions of $5 or $10 per month make a significant difference when combined with other community members' support. + +### Step 2: Join the Sponsor Community + +After becoming a sponsor: + +1. Join the [Telescope Discord](https://discord.gg/telescope) +2. Identify yourself as a sponsor to receive the sponsor role +3. Gain access to the sponsors-only channel where you can: + - Participate in discussions about future development + - Get priority support for issues + - Connect with other sponsors + +### Step 3: Share Your Feedback + +As a sponsor, your input is especially valuable: + +1. Share your experience using Telescope +2. Suggest improvements or new features +3. Participate in sponsor surveys that help guide development priorities + +Remember that sponsorship is just one way to contribute. You can also help by: + +- Submitting pull requests for bug fixes or features +- Improving documentation +- Answering questions on Discord or GitHub discussions +- Creating tutorials or examples for the community + +## Sponsoring as an Organization + +If you represent a company or organization that relies on Telescope for your projects, consider a corporate sponsorship: + +### Step 1: Determine Your Sponsorship Level + +Telescope offers several corporate sponsorship tiers: + +| Tier | Monthly Contribution | Key Benefits | +| ---- | -------------------- | ------------ | +| Bronze | $50-$199 | Logo in README, priority issue responses | +| Silver | $200-$499 | Medium logo in README, 2 hours of consulting monthly | +| Gold | $500-$999 | Large logo in README and docs, integration support | +| Platinum | $1000+ | Premium placement, dedicated support, prioritized features | + +Choose a tier that aligns with your organization's budget and the value Telescope provides to your development workflow. + +### Step 2: Establish a Sponsorship Agreement + +For corporate sponsorships: + +1. Contact the Telescope team at [sponsors@hyperweb.ai](mailto:sponsors@hyperweb.ai) +2. Explain your organization's needs and interest in sponsoring +3. Discuss any specific features or support your organization requires +4. Review and sign a sponsorship agreement + +Many organizations prefer to sponsor through [Open Collective](https://opencollective.com/telescope), which provides: + +- Formal invoicing +- Transparent fund allocation +- Tax-deductible contributions in many jurisdictions + +### Step 3: Leverage Your Sponsorship Benefits + +As a corporate sponsor, be sure to take advantage of your benefits: + +1. **Brand visibility** - Provide your logo for inclusion in the README and documentation +2. **Technical support** - Establish a direct communication channel with the maintainers +3. **Feature influence** - Share your product roadmap to help prioritize features that align with your needs +4. **Integration guidance** - Get advice on optimal use of Telescope in your specific context + +## Creating a Custom Sponsorship Program + +For larger organizations with specific needs, Telescope offers custom sponsorship arrangements: + +### Development Partnerships + +If your organization needs specific features: + +1. Outline your requirements in detail +2. Fund focused development of those features +3. Work closely with the maintainers during implementation +4. Receive early access to the features before general release + +### Educational Sponsorships + +If you're an educational institution: + +1. Sponsor Telescope at a discounted rate +2. Receive materials for teaching Cosmos SDK development +3. Get guest lectures or workshops from the maintainers +4. Provide students with learning resources and support + +### Ecosystem Grants + +If you're a foundation or grant-giving organization: + +1. Establish a grant program focused on specific ecosystem improvements +2. Define clear deliverables and milestones +3. Create a transparent reporting structure +4. Receive regular updates on development progress + +## Understanding How Your Sponsorship Helps + +When you sponsor Telescope, your contributions directly support: + +### Maintenance and Stability + +- Keeping the codebase up-to-date with the latest Cosmos SDK changes +- Addressing bug reports and security vulnerabilities +- Ensuring compatibility with new TypeScript versions +- Improving performance and reducing build times + +### New Feature Development + +- Adding support for new Cosmos SDK modules +- Implementing framework integrations (React, Vue, etc.) +- Enhancing developer experience with helper functions +- Creating new code generation capabilities + +### Documentation and Education + +- Expanding guides and tutorials +- Creating video content and examples +- Improving API documentation +- Developing starter templates + +### Community Building + +- Organizing community events and workshops +- Providing support on Discord and GitHub +- Mentoring new contributors +- Building relationships with related projects + +## Current Funding Priorities + +Telescope has several current funding goals that sponsors are helping to achieve: + +1. **Full-time Maintainer** (Target: $8,000/month) + - Status: 65% funded + - Impact: Faster issue resolution, more consistent development + +2. **Documentation Overhaul** (Target: $5,000) + - Status: 90% funded + - Impact: Comprehensive learning resources for developers of all skill levels + +3. **Vue Integration** (Target: $3,000) + - Status: 40% funded + - Impact: First-class support for Vue applications similar to existing React support + +4. **CosmWasm Extensions** (Target: $4,000) + - Status: 15% funded + - Impact: Better TypeScript generation for CosmWasm smart contracts + +## Sponsor Success Stories + +Here are how some existing sponsors have benefited from supporting Telescope: + +### Osmosis Labs + +"By sponsoring Telescope, we've been able to ensure that TypeScript developers can easily interact with the Osmosis DEX. This has significantly expanded our developer community and improved the quality of applications built on our platform." + +### Juno Network + +"Our sponsorship of Telescope helped accelerate the development of CosmWasm integration features, which has been critical for our smart contract platform. The direct support from the maintainers has also helped our own developers build better tools." + +### Keplr Wallet + +"Supporting Telescope has improved the developer experience for apps that integrate with Keplr Wallet. The type definitions and helper functions have reduced integration bugs and support requests, making our ecosystem stronger." + +## Conclusion + +Sponsoring Telescope is more than a financial contribution – it's an investment in the developer ecosystem that powers the applications built on Cosmos. Whether you're an individual developer or representing an organization, your support helps ensure that Telescope continues to evolve and serve the needs of the growing Cosmos community. + +To get started with sponsorship today: + +- Individual developers: Visit [GitHub Sponsors](https://github.com/sponsors/hyperweb-io) +- Organizations: Contact [sponsors@hyperweb.ai](mailto:sponsors@hyperweb.ai) or visit [Open Collective](https://opencollective.com/telescope) + +Thank you for considering supporting Telescope's mission to make Cosmos development more accessible and productive for TypeScript developers worldwide! \ No newline at end of file diff --git a/learn/stack.mdx b/learn/stack.mdx new file mode 100644 index 0000000000..bc2c2acb16 --- /dev/null +++ b/learn/stack.mdx @@ -0,0 +1,517 @@ +# Understanding Telescope's Technology Stack + +In this tutorial, we'll explore the technology stack that powers Telescope and the code it generates. By understanding the underlying technologies, you'll be better equipped to work with Telescope-generated code and troubleshoot any issues that arise. + +## The Building Blocks of Telescope + +Let's start by examining the key technologies that make up the Telescope ecosystem and how they work together. + +### TypeScript: The Foundation + +At its core, Telescope is built with TypeScript and generates TypeScript code. But why TypeScript instead of plain JavaScript? + +TypeScript provides several advantages for blockchain development: + +1. **Static typing** - Catches type errors at compile time rather than runtime +2. **Better tooling** - Improved autocompletion, refactoring, and documentation +3. **Code clarity** - Self-documenting code with explicit types +4. **Interface definitions** - Clear contracts between different parts of your code + +```typescript +// Example of TypeScript's benefits in blockchain code +interface Coin { + denom: string; + amount: string; +} + +// This will show an error at compile time if you forget a field +const atomCoin: Coin = { + denom: "uatom", + amount: "1000000" +}; + +// Without types, you might accidentally create invalid data +const invalidCoin = { + denom: "uatom", + // Missing amount field would cause runtime errors +}; +``` + +To work effectively with Telescope, you should have a basic understanding of TypeScript concepts like: + +- Interfaces and types +- Generics +- Union and intersection types +- Type assertions + +If you're new to TypeScript, the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) is an excellent resource. + +### Protocol Buffers: The Schema Language + +Cosmos SDK chains use Protocol Buffers (Protobuf) as their schema definition language. Telescope consumes these Protobuf definitions to generate TypeScript code. + +Protobuf offers several advantages: + +1. **Language neutrality** - Schemas can be used with many programming languages +2. **Structured data** - Clear message definitions +3. **Efficiency** - Compact binary representation +4. **Schema evolution** - Backward and forward compatibility + +Here's what a simple Protobuf definition looks like: + +```protobuf +syntax = "proto3"; +package cosmos.bank.v1beta1; + +message Coin { + string denom = 1; + string amount = 2; +} + +message Input { + string address = 1; + repeated Coin coins = 2; +} + +message Output { + string address = 1; + repeated Coin coins = 2; +} + +message MsgSend { + string from_address = 1; + string to_address = 2; + repeated Coin amount = 3; +} +``` + +Telescope reads these definitions and generates corresponding TypeScript types: + +```typescript +export interface Coin { + denom: string; + amount: string; +} + +export interface Input { + address: string; + coins: Coin[]; +} + +export interface Output { + address: string; + coins: Coin[]; +} + +export interface MsgSend { + fromAddress: string; + toAddress: string; + amount: Coin[]; +} +``` + +### CosmJS: The Runtime Library + +Telescope-generated code relies on CosmJS, a JavaScript/TypeScript library for interacting with Cosmos-based blockchains. CosmJS provides: + +1. **Client implementations** - For connecting to blockchain nodes +2. **Transaction creation** - Utilities for creating and signing transactions +3. **Query capabilities** - Tools for reading blockchain state +4. **Wallet integration** - Support for various signing methods + +When using Telescope-generated code, you'll often work with these CosmJS packages: + +- **@cosmjs/stargate** - High-level client for Cosmos SDK chains +- **@cosmjs/proto-signing** - Transaction signing with Protobuf +- **@cosmjs/tendermint-rpc** - Low-level RPC client +- **@cosmjs/amino** - Support for Amino encoding (legacy but still used) + +Here's a simple example of using CosmJS with Telescope-generated code: + +```typescript +import { StargateClient } from "@cosmjs/stargate"; +import { QueryClientImpl } from "./generated/cosmos/bank/v1beta1/query"; +import { createProtobufRpcClient } from "@cosmjs/stargate"; + +async function getBalance(address: string) { + // Connect to a Cosmos chain + const client = await StargateClient.connect("https://rpc.cosmoshub.strange.love"); + + // Create a Protobuf RPC client + const rpcClient = createProtobufRpcClient(client.queryClient); + + // Use the Telescope-generated client + const queryClient = new QueryClientImpl(rpcClient); + + // Query for balances + const response = await queryClient.AllBalances({ address }); + + return response.balances; +} +``` + +## The Inner Workings of Telescope + +Now that we understand the key technologies, let's explore how Telescope works under the hood. + +### Code Generation Process + +When you run Telescope, it follows these steps: + +1. **Parse Protobuf files** - Reads and parses all .proto files in the specified directories +2. **Build the AST** - Constructs an Abstract Syntax Tree representing the types and services +3. **Apply transformations** - Transforms the AST according to your configuration options +4. **Generate output files** - Produces TypeScript code from the transformed AST + +This process uses several key technologies: + +#### Abstract Syntax Tree (AST) Manipulation + +Telescope uses AST manipulation to transform Protobuf definitions into TypeScript code. This involves: + +1. Creating a tree representation of the code structure +2. Manipulating this tree according to rules +3. Generating TypeScript code from the modified tree + +This approach allows for fine-grained control over the generated code. + +#### Template-Based Generation + +For consistent patterns, Telescope uses code templates. These templates: + +1. Define the structure of generated files +2. Include placeholders for dynamic content +3. Get populated with specific data during generation + +This template-based approach ensures consistency across generated files. + +## Framework Integrations + +One of Telescope's powerful features is its ability to integrate with popular frameworks. Let's explore how these integrations work. + +### React Integration + +If you're building a React application, Telescope can generate React Query hooks for data fetching: + +```typescript +// Generated by Telescope with React integration enabled +export function useBalance( + rpcEndpoint: string, + params: QueryBalanceRequest, + options?: UseQueryOptions +): UseQueryResult { + return useQuery( + ['balance', rpcEndpoint, params], + () => getBalance(rpcEndpoint, params), + options + ); +} +``` + +You can then use these hooks in your components: + +```tsx +function BalanceDisplay({ address }: { address: string }) { + const { data, isLoading, error } = useBalance( + "https://rpc.cosmoshub.strange.love", + { address, denom: "uatom" } + ); + + if (isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return ( +
+

Balance

+

{data?.balance?.amount || "0"} {data?.balance?.denom || "uatom"}

+
+ ); +} +``` + +### Vue Integration + +For Vue applications, Telescope can generate Vue Query composables: + +```typescript +// Generated by Telescope with Vue integration enabled +export function useBalance( + rpcEndpoint: string, + params: QueryBalanceRequest, + options?: UseQueryOptions +) { + return useQuery( + ['balance', rpcEndpoint, params], + () => getBalance(rpcEndpoint, params), + options + ); +} +``` + +You can use these composables in your Vue components: + +```vue + + + +``` + +### CosmWasm Integration + +For projects working with CosmWasm smart contracts, Telescope integrates with @cosmwasm/ts-codegen to generate TypeScript clients: + +```typescript +// Example of using a generated CosmWasm client +import { CwCoin } from "./generated/CwCoin.client"; + +async function sendCoins() { + // Create a client instance + const client = new CwCoin("your-contract-address", signingClient); + + // Call a contract method + const result = await client.send({ + amount: "1000", + to: "recipient-address" + }); + + console.log("Transaction hash:", result.transactionHash); +} +``` + +## Runtime Environment Considerations + +Telescope-generated code can run in different environments, each with its own considerations. + +### Browser Environment + +When running in browsers, you need to be aware of: + +#### Polyfills for Node.js Built-ins + +Many CosmJS packages use Node.js built-ins that aren't available in browsers. You'll need polyfills for: + +- `Buffer` +- `stream` +- `crypto` +- `path` +- `os` + +Here's how to configure webpack for these polyfills: + +```javascript +// webpack.config.js +module.exports = { + // ... + resolve: { + fallback: { + "stream": require.resolve("stream-browserify"), + "buffer": require.resolve("buffer/"), + "crypto": require.resolve("crypto-browserify"), + // Other polyfills... + } + }, + plugins: [ + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + process: 'process/browser', + }), + ] +}; +``` + +#### CORS Considerations + +Browser security policies may restrict connections to blockchain RPC endpoints. Solutions include: + +1. Using CORS-enabled endpoints +2. Setting up a proxy server +3. Using a service like [cors-anywhere](https://github.com/Rob--W/cors-anywhere/) + +```typescript +// Using a CORS proxy +const corsProxy = "https://cors-anywhere.herokuapp.com/"; +const rpcEndpoint = "https://rpc.cosmoshub.strange.love"; +const client = await StargateClient.connect(corsProxy + rpcEndpoint); +``` + +### Node.js Environment + +Node.js applications have fewer compatibility issues since CosmJS is primarily developed for Node.js: + +```typescript +// Node.js example +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { SigningStargateClient } from "@cosmjs/stargate"; + +async function sendTokens() { + // Create a wallet + const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + "your secret mnemonic here", + { prefix: "cosmos" } + ); + + // Create a signing client + const client = await SigningStargateClient.connectWithSigner( + "https://rpc.cosmoshub.strange.love", + wallet + ); + + // Send tokens + const result = await client.sendTokens( + "sender-address", + "recipient-address", + [{ denom: "uatom", amount: "1000000" }], + { amount: [{ denom: "uatom", amount: "5000" }], gas: "200000" } + ); + + console.log("Transaction hash:", result.transactionHash); +} +``` + +### Mobile Environment (React Native) + +For React Native applications, additional considerations include: + +1. Native cryptography modules +2. Platform-specific networking +3. Memory constraints + +You may need to use libraries like `react-native-get-random-values` and `@walletconnect/react-native-compat`. + +## Development Tools and Best Practices + +To work effectively with Telescope, consider adopting these tools and practices: + +### Recommended Development Environment + +- **IDE**: Visual Studio Code with TypeScript support +- **Extensions**: + - ESLint for code linting + - Prettier for code formatting + - Proto3 for .proto file syntax highlighting +- **Node.js**: Use version 16+ for optimal performance +- **Package Manager**: Yarn 1.x for consistent dependency management + +### Testing Strategies + +When working with Telescope-generated code: + +1. **Mock the RPC layer** for unit tests: + +```typescript +// Example of mocking a query client +const mockQueryClient = { + balance: jest.fn().mockResolvedValue({ + balance: { denom: "uatom", amount: "1000000" } + }) +}; + +const client = new QueryClientImpl(mockQueryClient); +const result = await client.Balance({ address: "cosmos1...", denom: "uatom" }); +expect(result.balance.amount).toBe("1000000"); +``` + +2. **Use test networks** for integration tests: + +```typescript +// Integration test with test network +const testnetEndpoint = "https://rpc.sentry-01.theta-testnet.polypore.xyz"; +const client = await StargateClient.connect(testnetEndpoint); +const balance = await client.getAllBalances("cosmos1test..."); +expect(balance).toBeDefined(); +``` + +3. **Consider specialized testing tools** like @cosmjs/starship for simulated environments + +### Performance Optimization + +To optimize the performance of applications using Telescope-generated code: + +1. **Implement caching** for frequently accessed data: + +```typescript +// Simple cache implementation +const cache = new Map(); +const CACHE_TTL = 30000; // 30 seconds + +async function getCachedBalance(address: string) { + const cacheKey = `balance:${address}`; + const cachedData = cache.get(cacheKey); + + if (cachedData && cachedData.timestamp > Date.now() - CACHE_TTL) { + return cachedData.data; + } + + const client = await StargateClient.connect("https://rpc.cosmoshub.strange.love"); + const balance = await client.getAllBalances(address); + + cache.set(cacheKey, { + timestamp: Date.now(), + data: balance + }); + + return balance; +} +``` + +2. **Batch queries** where possible: + +```typescript +// Instead of multiple individual queries +const [balanceResponse, stakingResponse, distributionResponse] = await Promise.all([ + queryClient.cosmos.bank.v1beta1.allBalances({ address }), + queryClient.cosmos.staking.v1beta1.delegatorDelegations({ delegatorAddr: address }), + queryClient.cosmos.distribution.v1beta1.delegationRewards({ delegatorAddress: address, validatorAddress }) +]); +``` + +3. **Use pagination** for large result sets: + +```typescript +// Paginated query example +let nextKey = null; +const allValidators = []; + +do { + const response = await queryClient.cosmos.staking.v1beta1.validators({ + pagination: { + key: nextKey, + limit: "100" + } + }); + + allValidators.push(...response.validators); + nextKey = response.pagination.nextKey; +} while (nextKey && nextKey.length > 0); +``` + +## Conclusion + +Understanding Telescope's technology stack empowers you to build better applications for the Cosmos ecosystem. The combination of TypeScript, Protocol Buffers, and CosmJS provides a solid foundation for developing robust blockchain applications. + +By leveraging Telescope's integrations with popular frameworks like React and Vue, you can build user-friendly interfaces that interact seamlessly with Cosmos SDK chains. And by considering the runtime environment and following best practices, you can ensure your applications are performant and reliable. + +As you continue your journey with Telescope, remember that the community is a valuable resource. Don't hesitate to ask questions in the Cosmos Developer Discord or contribute to the project on GitHub. + +Happy building! \ No newline at end of file diff --git a/learn/stargate-clients.mdx b/learn/stargate-clients.mdx new file mode 100644 index 0000000000..c4ee23de8a --- /dev/null +++ b/learn/stargate-clients.mdx @@ -0,0 +1,588 @@ +# Working with Stargate Clients + +In this tutorial, we'll learn how to use Stargate clients with Telescope-generated types to communicate with Cosmos SDK blockchains. We'll cover everything from basic setup to advanced usage patterns to help you build robust blockchain applications. + +## Understanding Stargate Clients + +Stargate clients are the primary way to interact with Cosmos SDK blockchains from your JavaScript or TypeScript applications. They provide an interface to: + +1. Query blockchain data (account balances, delegations, governance proposals, etc.) +2. Send transactions (transfer tokens, delegate to validators, vote on proposals, etc.) +3. Subscribe to blockchain events + +Telescope enhances these clients by generating TypeScript types that provide type safety and autocompletion for all blockchain interactions. + +## Types of Clients + +Let's start by understanding the different client types available: + +### Query-only Clients + +These clients can only read data from the blockchain, not modify it: + +- **StargateClient**: Basic read-only client for common operations +- **QueryClient**: Advanced read-only client that can be extended with module-specific query methods + +### Transaction Clients + +These clients can both read data and send transactions: + +- **SigningStargateClient**: The main client for sending signed transactions +- **Telescope-generated clients**: Chain-specific clients like `getSigningOsmosisClient` + +Let's learn how to use each of these clients, starting with the simplest ones. + +## Setting Up a Basic StargateClient + +First, let's create a simple StargateClient to query basic blockchain information: + +```typescript +import { StargateClient } from "@cosmjs/stargate"; + +async function queryBasicInfo() { + // Connect to a Cosmos SDK chain (in this case, Cosmos Hub) + const client = await StargateClient.connect("https://rpc.cosmos.network"); + + try { + // Get the chain ID + const chainId = await client.getChainId(); + console.log("Chain ID:", chainId); + + // Get the current block height + const height = await client.getHeight(); + console.log("Current height:", height); + + // Get account balance + const address = "cosmos1..."; // Replace with an actual address + const balance = await client.getAllBalances(address); + console.log("Account balance:", balance); + + // Get account information + const account = await client.getAccount(address); + console.log("Account details:", account); + } finally { + // Always disconnect when done to free resources + client.disconnect(); + } +} + +queryBasicInfo().catch(console.error); +``` + +This client is perfect for simple read-only operations, but for more advanced queries, we'll need to use a QueryClient. + +## Working with QueryClient and Extensions + +The QueryClient is more powerful because it can be extended with module-specific functionality: + +```typescript +import { QueryClient, setupStargateClient } from "@cosmjs/stargate"; +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; +import { setupBankExtension, BankExtension } from "@cosmjs/stargate"; + +// Define a type that includes our extensions +type ExtendedQueryClient = QueryClient & { + stargateClient: ReturnType; + bank: BankExtension; +}; + +async function queryWithExtensions() { + // First, create a Tendermint client + const tmClient = await Tendermint34Client.connect("https://rpc.cosmos.network"); + + try { + // Create a query client with extensions + const queryClient = QueryClient.withExtensions( + tmClient, + setupStargateClient, + setupBankExtension + ) as ExtendedQueryClient; + + // Use the base stargate client extension + const chainId = await queryClient.stargateClient.getChainId(); + const height = await queryClient.stargateClient.getHeight(); + console.log(`Chain: ${chainId}, Height: ${height}`); + + // Use the bank extension + const address = "cosmos1..."; // Replace with an actual address + const balances = await queryClient.bank.allBalances(address); + console.log("Balances:", balances); + + const supply = await queryClient.bank.totalSupply(); + console.log("Total supply:", supply); + } finally { + tmClient.disconnect(); + } +} + +queryWithExtensions().catch(console.error); +``` + +## Creating a Client with Telescope-generated Types + +Telescope enhances your interaction with Cosmos chains by generating type-safe clients. Let's see how to use them: + +```typescript +import { createRPCQueryClient } from "./codegen/cosmos"; + +async function queryWithTelescopeTypes() { + // Create a Telescope-generated query client + const client = await createRPCQueryClient({ + rpcEndpoint: "https://rpc.cosmos.network" + }); + + try { + // Query bank module with full type safety + const address = "cosmos1..."; // Replace with an actual address + const { balances } = await client.cosmos.bank.v1beta1.allBalances({ address }); + console.log("Balances:", balances); + + // Query staking module + const { validators } = await client.cosmos.staking.v1beta1.validators({}); + console.log(`Found ${validators.length} validators`); + + // Query governance proposals + const { proposals } = await client.cosmos.gov.v1beta1.proposals({ + proposalStatus: 0, // All proposals + voter: "", + depositor: "" + }); + console.log(`Found ${proposals.length} governance proposals`); + } finally { + client.disconnect(); + } +} + +queryWithTelescopeTypes().catch(console.error); +``` + +## Signing and Broadcasting Transactions + +Now let's learn how to send transactions using the SigningStargateClient: + +```typescript +import { SigningStargateClient, GasPrice } from "@cosmjs/stargate"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { registry } from "./codegen/registry"; + +async function sendTransaction() { + // First, create a wallet (in production, use a more secure method) + const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + "your mnemonic phrase here", // Never hardcode this in production! + { prefix: "cosmos" } // Address prefix for the chain + ); + + // Get the first account from the wallet + const [firstAccount] = await wallet.getAccounts(); + const sender = firstAccount.address; + + // Create a signing client + const client = await SigningStargateClient.connectWithSigner( + "https://rpc.cosmos.network", + wallet, + { + registry, // Use Telescope-generated registry + gasPrice: GasPrice.fromString("0.025uatom") // Set an appropriate gas price + } + ); + + try { + // Example: Send tokens + const recipient = "cosmos1recipient..."; + const amount = [{ denom: "uatom", amount: "1000000" }]; // 1 ATOM in microatoms + + // Estimate fee through simulation (recommended) + const gasEstimated = await client.simulate(sender, [ + { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: { + fromAddress: sender, + toAddress: recipient, + amount: amount + } + } + ], ""); + + // Add a safety margin to gas estimate + const gasLimit = Math.round(gasEstimated * 1.3); + + // Create the fee + const fee = { + amount: [{ denom: "uatom", amount: "5000" }], + gas: gasLimit.toString() + }; + + // Send the transaction + const result = await client.signAndBroadcast( + sender, + [ + { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: { + fromAddress: sender, + toAddress: recipient, + amount: amount + } + } + ], + fee, + "Transaction from Telescope tutorial" // Memo + ); + + console.log("Transaction result:", result); + console.log("Transaction hash:", result.transactionHash); + + // Check if transaction was successful + if (result.code === 0) { + console.log("Transaction successful!"); + } else { + console.error("Transaction failed with code:", result.code); + console.error("Error message:", result.rawLog); + } + } finally { + client.disconnect(); + } +} + +sendTransaction().catch(console.error); +``` + +## Using Telescope-generated Message Types + +Telescope generates TypeScript types for all the messages in a blockchain. Let's use them for better type safety: + +```typescript +import { SigningStargateClient } from "@cosmjs/stargate"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { registry } from "./codegen/registry"; +import { MsgSend } from "./codegen/cosmos/bank/v1beta1/tx"; + +async function sendTypedTransaction() { + // Set up wallet and client as before + const wallet = await DirectSecp256k1HdWallet.fromMnemonic("..."); + const [account] = await wallet.getAccounts(); + const client = await SigningStargateClient.connectWithSigner( + "https://rpc.cosmos.network", + wallet, + { registry } + ); + + try { + // Create a MsgSend using Telescope-generated types + const sendMsg = MsgSend.fromPartial({ + fromAddress: account.address, + toAddress: "cosmos1recipient...", + amount: [{ denom: "uatom", amount: "1000000" }] + }); + + // Prepare the transaction + const msgSend = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: sendMsg + }; + + // Send the transaction + const result = await client.signAndBroadcast( + account.address, + [msgSend], + { + amount: [{ denom: "uatom", amount: "5000" }], + gas: "200000" + } + ); + + console.log("Transaction hash:", result.transactionHash); + } finally { + client.disconnect(); + } +} + +sendTypedTransaction().catch(console.error); +``` + +## Using Chain-Specific Signing Clients + +Telescope generates chain-specific clients that make it even easier to interact with specific blockchains: + +```typescript +import { getSigningOsmosisClient } from "osmojs"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; + +async function useChainSpecificClient() { + // Create a wallet for the Osmosis chain + const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + "your mnemonic here", + { prefix: "osmo" } // Osmosis address prefix + ); + + // Get the first account + const [account] = await wallet.getAccounts(); + + // Create a chain-specific signing client + const client = await getSigningOsmosisClient({ + rpcEndpoint: "https://rpc.osmosis.zone", + signer: wallet + }); + + try { + // Now you can use methods specific to Osmosis + // For example, to swap tokens: + const swapMsg = { + typeUrl: "/osmosis.gamm.v1beta1.MsgSwapExactAmountIn", + value: { + sender: account.address, + routes: [ + { + poolId: "1", + tokenOutDenom: "uosmo" + } + ], + tokenIn: { + denom: "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", + amount: "1000000" + }, + tokenOutMinAmount: "500000" + } + }; + + // Send the transaction + const result = await client.signAndBroadcast( + account.address, + [swapMsg], + { + amount: [{ denom: "uosmo", amount: "5000" }], + gas: "300000" + } + ); + + console.log("Swap completed, txhash:", result.transactionHash); + } finally { + client.disconnect(); + } +} + +useChainSpecificClient().catch(console.error); +``` + +## Setting Up Registry and Amino Types + +When working with custom chains or custom message types, you might need to configure the registry and amino types: + +```typescript +import { SigningStargateClient, AminoTypes } from "@cosmjs/stargate"; +import { Registry } from "@cosmjs/proto-signing"; +import { + cosmosAminoConverters, cosmosProtoRegistry, + ibcAminoConverters, ibcProtoRegistry, + osmosisAminoConverters, osmosisProtoRegistry +} from "osmojs"; + +async function setupCustomRegistryAndAmino() { + // Combine registries from different modules + const registry = new Registry([ + ...cosmosProtoRegistry, + ...ibcProtoRegistry, + ...osmosisProtoRegistry + ]); + + // Combine amino converters from different modules + const aminoTypes = new AminoTypes({ + ...cosmosAminoConverters, + ...ibcAminoConverters, + ...osmosisAminoConverters + }); + + // Create a client with custom registry and amino types + const client = await SigningStargateClient.connectWithSigner( + "https://rpc.osmosis.zone", + wallet, + { + registry, + aminoTypes + } + ); + + // Now you can use this client to send any supported message type + // The registry ensures proper encoding/decoding of Protobuf messages + // The aminoTypes ensures compatibility with wallets that use Amino encoding +} +``` + +## Managing Multiple Chain Configurations + +In real-world applications, you might need to interact with multiple chains. Here's how to manage configurations: + +```typescript +import { GasPrice } from "@cosmjs/stargate"; + +// Define configurations for different chains +const chainConfigs = { + "cosmoshub-4": { + rpcEndpoint: "https://rpc.cosmos.network", + restEndpoint: "https://rest.cosmos.network", + prefix: "cosmos", + gasPrice: GasPrice.fromString("0.025uatom"), + feeDenom: "uatom" + }, + "osmosis-1": { + rpcEndpoint: "https://rpc.osmosis.zone", + restEndpoint: "https://rest.osmosis.zone", + prefix: "osmo", + gasPrice: GasPrice.fromString("0.025uosmo"), + feeDenom: "uosmo" + }, + "juno-1": { + rpcEndpoint: "https://rpc.juno.omniflix.co", + restEndpoint: "https://rest.juno.omniflix.co", + prefix: "juno", + gasPrice: GasPrice.fromString("0.025ujuno"), + feeDenom: "ujuno" + } +}; + +async function createClientForChain(chainId, wallet) { + const config = chainConfigs[chainId]; + if (!config) { + throw new Error(`Config not found for chain: ${chainId}`); + } + + // Create a signing client for the selected chain + return SigningStargateClient.connectWithSigner( + config.rpcEndpoint, + wallet, + { + prefix: config.prefix, + gasPrice: config.gasPrice + } + ); +} +``` + +## Reading Transaction Results + +When you send a transaction, you get back a result object with important information. Here's how to interpret it: + +```typescript +// After sending a transaction +const result = await client.signAndBroadcast(sender, messages, fee); + +// Basic transaction info +console.log("Transaction hash:", result.transactionHash); +console.log("Block height:", result.height); +console.log("Gas used:", result.gasUsed, "out of", result.gasWanted); + +// Check if transaction was successful +if (result.code === 0) { + console.log("Transaction successful!"); +} else { + console.error("Transaction failed with code:", result.code); + console.error("Error message:", result.rawLog); +} + +// Parse events and attributes +if (result.events) { + console.log("Events from transaction:"); + result.events.forEach(event => { + console.log(`Event type: ${event.type}`); + event.attributes.forEach(attr => { + console.log(` ${attr.key}: ${attr.value}`); + }); + }); + + // Example: Extract a proposal ID from a submit proposal transaction + const submitEvent = result.events.find(e => e.type === "submit_proposal"); + if (submitEvent) { + const proposalIdAttr = submitEvent.attributes.find(a => a.key === "proposal_id"); + if (proposalIdAttr) { + console.log("Created proposal ID:", proposalIdAttr.value); + } + } +} +``` + +## Error Handling and Retries + +Blockchain interactions can fail for various reasons. Here's how to handle common errors and implement retries: + +```typescript +async function sendTransactionWithRetry(client, sender, messages, fee, maxRetries = 3) { + let retries = 0; + + while (retries < maxRetries) { + try { + const result = await client.signAndBroadcast(sender, messages, fee); + + // Check for failure codes + if (result.code !== 0) { + console.error(`Transaction failed with code ${result.code}: ${result.rawLog}`); + + // Handle specific error cases + if (result.rawLog.includes("insufficient fees")) { + // Increase fees and retry + fee.amount[0].amount = (parseInt(fee.amount[0].amount) * 1.5).toString(); + console.log("Retrying with higher fees:", fee); + retries++; + continue; + } + + if (result.rawLog.includes("out of gas")) { + // Increase gas limit and retry + fee.gas = (parseInt(fee.gas) * 1.5).toString(); + console.log("Retrying with higher gas limit:", fee); + retries++; + continue; + } + + // For other errors, don't retry + return result; + } + + // Success + return result; + } catch (error) { + console.error(`Attempt ${retries + 1} failed:`, error.message); + + // Handle network errors + if (error.message.includes("socket hang up") || + error.message.includes("connection refused") || + error.message.includes("timeout")) { + console.log("Network error, retrying..."); + retries++; + // Wait before retrying (exponential backoff) + await new Promise(r => setTimeout(r, 1000 * Math.pow(2, retries))); + continue; + } + + // For other errors, don't retry + throw error; + } + } + + throw new Error(`Failed after ${maxRetries} attempts`); +} +``` + +## Best Practices + +To make the most of Stargate clients in your applications: + +1. **Always disconnect clients when done** to prevent resource leaks +2. **Use a proper registry** with all your message types registered +3. **Implement proper error handling** for network issues and other failures +4. **Simulate transactions** before broadcasting to estimate gas +5. **Add a safety margin** to gas estimates (usually 1.3x to 1.5x) +6. **Use appropriate gas prices** for each chain +7. **Include informative memos** in transactions for better traceability +8. **Parse transaction events** to extract important information +9. **Implement retry mechanisms** for transient failures +10. **Use type-safe message composition** with Telescope-generated types + +## Next Steps + +Now that you understand how to use Stargate clients with Telescope-generated types, you can: + +- Learn about [creating signers](./creating-signers.md) for transaction authorization +- Explore [composing messages](./composing-messages.md) for different transaction types +- Understand [calculating fees](./calculating-fees.md) for your transactions + +By combining these skills, you'll be able to build robust applications that interact with any Cosmos SDK blockchain in a type-safe way. \ No newline at end of file diff --git a/learn/troubleshooting.mdx b/learn/troubleshooting.mdx new file mode 100644 index 0000000000..75f72914e3 --- /dev/null +++ b/learn/troubleshooting.mdx @@ -0,0 +1,662 @@ +# Solving Common Problems in Telescope Projects + +In this tutorial, we'll walk through common issues that you might encounter when working with Telescope-generated code and provide step-by-step solutions for each problem. Whether you're facing build errors, runtime issues, or type problems, this guide will help you diagnose and fix them. + +## Understanding Telescope Error Types + +When working with Telescope, you might encounter several types of errors: + +1. **Build-time errors** - Issues that occur during code generation +2. **Compile-time errors** - TypeScript errors when compiling your application +3. **Runtime errors** - Issues that occur when your application is running +4. **Environment-specific issues** - Problems related to specific frameworks or platforms + +Let's tackle each category one by one. + +## Build-Time Errors + +### Problem: Proto Parsing Errors + +One of the most common issues is errors when Telescope tries to parse proto files. + +**Symptoms:** +``` +Error parsing proto file: syntax error, unexpected IDENTIFIER, expecting MESSAGE +``` + +**Solution:** + +1. First, identify which proto file is causing the issue: + +```bash +npx telescope --debug +``` + +2. Look for syntax errors in the proto file. Common issues include: + - Missing semicolons + - Incorrect indentation + - Invalid message or field names + - Using reserved keywords + +For example, if you have: + +```protobuf +message User + string name = 1; + int32 age = 2; +} +``` + +Fix it by adding the opening brace and semicolons: + +```protobuf +message User { + string name = 1; + int32 age = 2; +} +``` + +### Problem: Missing Import Files + +**Symptoms:** +``` +Cannot find imported file: "google/protobuf/timestamp.proto" +``` + +**Solution:** + +1. Ensure you have all needed proto files available in your proto directories +2. For standard Google protobuf definitions, install them: + +```bash +mkdir -p proto/google/protobuf +curl -o proto/google/protobuf/timestamp.proto https://raw.githubusercontent.com/protocolbuffers/protobuf/main/src/google/protobuf/timestamp.proto +``` + +3. Update your Telescope configuration to include all proto directories: + +```javascript +module.exports = { + protoDirs: [ + 'proto', + 'third_party/proto' + ], + // ... +}; +``` + +## Compile-Time Errors + +### Problem: Long Type Errors + +Telescope generates complex types that can sometimes lead to lengthy, hard-to-understand type errors. + +**Symptoms:** +``` +Type 'DeepPartial<{ amount: string; denom: string; }>' is not assignable to type... +[extremely long error message follows] +``` + +**Solution:** + +1. Enable the `useDeepPartial` option in your configuration: + +```javascript +module.exports = { + // ... + options: { + useDeepPartial: true + } +}; +``` + +2. Use type assertions in problematic areas: + +```typescript +// From this: +const amount: Coin = { amount: "1000", denom: "uatom" }; + +// To this: +const amount = { amount: "1000", denom: "uatom" } as Coin; +``` + +3. In extreme cases, use `@ts-ignore` comments for lines that TypeScript struggles with: + +```typescript +// @ts-ignore - Complex nested type issue +const result = await queryClient.cosmos.bank.v1beta1.allBalances({ address }); +``` + +### Problem: Module Resolution Issues + +**Symptoms:** +``` +Cannot find module '@cosmos/telescope-generated/cosmos.bank.v1beta1' +``` + +**Solution:** + +1. Check your `tsconfig.json` settings: + +```json +{ + "compilerOptions": { + "moduleResolution": "node", + "baseUrl": ".", + "paths": { + "@cosmos/*": ["./src/generated/*"] + } + } +} +``` + +2. Ensure your import paths match the output structure: + +```typescript +// If generated code is at src/generated/cosmos/bank/v1beta1/query.ts +import { QueryClientImpl } from "./src/generated/cosmos/bank/v1beta1/query"; + +// Or with path aliasing: +import { QueryClientImpl } from "@cosmos/telescope-generated/cosmos/bank/v1beta1/query"; +``` + +3. Regenerate your code if paths have changed: + +```bash +npx telescope --clear +``` + +## Runtime Errors + +### Problem: Webpack Polyfill Issues in Browser + +**Symptoms:** +``` +Uncaught ReferenceError: Buffer is not defined +``` + +or + +``` +Module not found: Error: Can't resolve 'stream' in '...' +``` + +**Solution:** + +For Create React App projects: + +1. Install `react-app-rewired` and `customize-cra`: + +```bash +npm install --save-dev react-app-rewired customize-cra +``` + +2. Create a `config-overrides.js` file in your project root: + +```javascript +const { override, addWebpackPlugin, addWebpackAlias } = require('customize-cra'); +const webpack = require('webpack'); + +module.exports = override( + // Add polyfills + addWebpackPlugin( + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + process: 'process/browser', + }) + ), + // Add aliases for Node.js core modules + addWebpackAlias({ + 'stream': 'stream-browserify', + 'path': 'path-browserify', + 'crypto': 'crypto-browserify', + 'http': 'stream-http', + 'https': 'https-browserify', + 'os': 'os-browserify/browser', + }) +); +``` + +3. Modify your `package.json` scripts: + +```json +"scripts": { + "start": "react-app-rewired start", + "build": "react-app-rewired build", + "test": "react-app-rewired test" +} +``` + +4. Install the polyfill packages: + +```bash +npm install buffer process stream-browserify path-browserify crypto-browserify stream-http https-browserify os-browserify +``` + +For other bundlers like webpack: + +```javascript +// webpack.config.js +module.exports = { + // ... + resolve: { + fallback: { + "stream": require.resolve("stream-browserify"), + "buffer": require.resolve("buffer/"), + "crypto": require.resolve("crypto-browserify"), + "path": require.resolve("path-browserify"), + "http": require.resolve("stream-http"), + "https": require.resolve("https-browserify"), + "os": require.resolve("os-browserify/browser") + } + }, + plugins: [ + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + process: 'process/browser', + }), + ] +}; +``` + +### Problem: Chain Connection Issues + +**Symptoms:** +``` +Error: socket hang up +``` + +or + +``` +Error: Request failed with status code 404 +``` + +**Solution:** + +1. Check your RPC endpoint URL: + +```typescript +// Instead of this (which might be outdated or incorrect) +const client = await StargateClient.connect("https://rpc.cosmos.hub.com"); + +// Try a known working public endpoint +const client = await StargateClient.connect("https://rpc.cosmoshub.strange.love"); +``` + +2. Use error handling and fallback endpoints: + +```typescript +async function connectWithFallbacks() { + const endpoints = [ + "https://rpc.cosmoshub.strange.love", + "https://cosmos-rpc.polkachu.com", + "https://rpc-cosmoshub.blockapsis.com" + ]; + + for (const endpoint of endpoints) { + try { + console.log(`Trying to connect to ${endpoint}...`); + const client = await StargateClient.connect(endpoint); + console.log(`Connected to ${endpoint}`); + return client; + } catch (error) { + console.error(`Failed to connect to ${endpoint}:`, error.message); + } + } + + throw new Error("Failed to connect to any RPC endpoint"); +} +``` + +3. Enable network monitoring to see the actual requests and responses: + +```typescript +// In your browser's console +const client = await StargateClient.connect( + "https://rpc.cosmoshub.strange.love", + { logger: new ConsoleLogger("debug") } +); +``` + +## Framework-Specific Issues + +### React Query Integration Issues + +**Symptoms:** +``` +TypeError: Cannot read property 'fetchQuery' of undefined +``` + +**Solution:** + +1. Ensure you have the correct React Query setup: + +```tsx +// App.tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +const queryClient = new QueryClient(); + +function App() { + return ( + + + + ); +} +``` + +2. Check your hook usage: + +```tsx +// Component.tsx +import { useBalance } from '../generated/cosmos/bank/v1beta1/query.hooks'; + +function WalletBalance({ address }) { + const { data, isLoading, error } = useBalance( + "https://rpc.cosmoshub.strange.love", + { address, denom: "uatom" } + ); + + if (isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return
Balance: {parseFloat(data?.balance?.amount || "0") / 1_000_000} ATOM
; +} +``` + +### Next.js Integration Issues + +**Symptoms:** +``` +Error: Cannot serialize type BigInt +``` + +**Solution:** + +Create a custom serialization helper: + +```typescript +// utils/serializers.ts +export function serializable(obj: T): T { + return JSON.parse( + JSON.stringify(obj, (key, value) => + typeof value === 'bigint' ? value.toString() : value + ) + ); +} +``` + +Then use it in your Next.js pages: + +```typescript +// pages/balances.tsx +import { serializable } from '../utils/serializers'; + +export async function getServerSideProps() { + const client = await StargateClient.connect("https://rpc.cosmoshub.strange.love"); + const balance = await client.getAllBalances("cosmos1..."); + + return { + props: { + balance: serializable(balance) + } + }; +} +``` + +## Debugging Strategies + +Let's explore some general debugging strategies for Telescope projects: + +### 1. Enable Verbose Logging + +Enable debug logging in Telescope during code generation: + +```bash +npx telescope --debug +``` + +Enable logging in CosmJS clients: + +```typescript +import { ConsoleLogger } from "@cosmjs/stargate"; + +const client = await StargateClient.connect( + "https://rpc.cosmoshub.strange.love", + { logger: new ConsoleLogger("debug") } +); +``` + +### 2. Inspect Network Requests + +For browser applications, use the Network tab in Developer Tools: + +1. Open your browser's Developer Tools (F12) +2. Navigate to the Network tab +3. Filter for "Fetch/XHR" requests +4. Locate requests to your RPC endpoints +5. Examine the request and response payloads + +For Node.js applications, use a tool like `axios-debug-log`: + +```typescript +import axios from "axios"; +import axiosDebug from "axios-debug-log"; + +axiosDebug(axios); +``` + +### 3. Capture Detailed Error Information + +Create a helper function to extract more information from errors: + +```typescript +function logDetailedError(error: any) { + console.error({ + message: error.message, + name: error.name, + code: error.code, + data: error.data, + stack: error.stack, + response: error.response && { + status: error.response.status, + statusText: error.response.statusText, + data: error.response.data, + headers: error.response.headers, + }, + }); +} +``` + +Use this in try/catch blocks: + +```typescript +try { + const result = await client.sendTokens(...); +} catch (error) { + logDetailedError(error); +} +``` + +### 4. Use TypeScript Playground + +For complex type issues, you can use the [TypeScript Playground](https://www.typescriptlang.org/play/) to isolate and experiment with problematic types. + +## Real World Example: Debugging a Transaction Broadcast Issue + +Let's walk through a complete example of debugging a common issue: a failed transaction broadcast. + +### The Problem + +You're trying to send tokens from one address to another, but the transaction fails with a cryptic error. + +### Step 1: Implement Basic Error Handling + +```typescript +import { SigningStargateClient } from "@cosmjs/stargate"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; + +async function sendTokens() { + try { + const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + "your secret mnemonic here", + { prefix: "cosmos" } + ); + + const [firstAccount] = await wallet.getAccounts(); + console.log("Sender address:", firstAccount.address); + + const client = await SigningStargateClient.connectWithSigner( + "https://rpc.cosmoshub.strange.love", + wallet, + { logger: new ConsoleLogger("debug") } + ); + + const result = await client.sendTokens( + firstAccount.address, + "cosmos1recipient", + [{ denom: "uatom", amount: "1000" }], + { amount: [{ denom: "uatom", amount: "5000" }], gas: "200000" } + ); + + console.log("Transaction result:", result); + } catch (error) { + console.error("Transaction failed:", { + message: error.message, + code: error.code, + data: error.data, + }); + } +} +``` + +### Step 2: Run and Identify the Issue + +When you run this code, you might see an error like: + +``` +Transaction failed: { + message: "Account does not exist on chain. Send some tokens there before trying to query sequence.", + code: undefined, + data: undefined +} +``` + +### Step 3: Diagnose the Problem + +This error indicates that the sender account doesn't exist on the blockchain yet. This happens when an account has not yet received any tokens or interacted with the blockchain. + +### Step 4: Fix the Issue + +There are two possible solutions: + +1. **If you're using a test account**: Send some tokens to this account first from a faucet or another account. + +2. **If you're using the wrong network**: Make sure your mnemonic corresponds to an account on the network you're connecting to. + +```typescript +// For testnet, use a testnet endpoint and prefix +const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + "your secret mnemonic here", + { prefix: "cosmos" } +); + +// Use a testnet endpoint +const client = await SigningStargateClient.connectWithSigner( + "https://rpc.testnet.cosmos.network", + wallet +); +``` + +### Step 5: Verify the Solution + +Add a check to ensure the account exists and has sufficient funds before attempting to send tokens: + +```typescript +async function sendTokensWithVerification() { + try { + const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + "your secret mnemonic here", + { prefix: "cosmos" } + ); + + const [firstAccount] = await wallet.getAccounts(); + const address = firstAccount.address; + console.log("Sender address:", address); + + // First create a query client to check the account + const client = await SigningStargateClient.connectWithSigner( + "https://rpc.cosmoshub.strange.love", + wallet + ); + + // Check if the account exists and has sufficient funds + const balance = await client.getBalance(address, "uatom"); + console.log("Account balance:", balance); + + const amountToSend = "1000"; + const fee = "5000"; + const totalNeeded = parseInt(amountToSend) + parseInt(fee); + + if (!balance.amount || parseInt(balance.amount) < totalNeeded) { + throw new Error(`Insufficient funds. Have ${balance.amount || 0} uatom, need ${totalNeeded} uatom.`); + } + + // Now proceed with the transaction + const result = await client.sendTokens( + address, + "cosmos1recipient", + [{ denom: "uatom", amount: amountToSend }], + { amount: [{ denom: "uatom", amount: fee }], gas: "200000" } + ); + + console.log("Transaction result:", result); + return result; + } catch (error) { + console.error("Transaction failed:", { + message: error.message, + code: error.code, + data: error.data, + }); + throw error; + } +} +``` + +## Getting Help from the Community + +If you've tried all the troubleshooting steps and still can't resolve your issue, it's time to seek help from the community. + +### 1. Prepare a Clear Description + +Before reaching out, prepare a clear description of your issue: + +- What you're trying to accomplish +- What you've tried so far +- The exact error messages you're seeing +- Your environment details (Node.js version, Telescope version, etc.) +- A minimal reproducible example if possible + +### 2. Join the Cosmos Developer Discord + +The Cosmos Developer Discord is a great place to get help: + +1. Join the [Cosmos Developer Discord](https://discord.gg/cosmosnetwork) +2. Navigate to the #developers channel or the #telescope channel +3. Post your question with the information you prepared + +### 3. Submit a GitHub Issue + +For bugs or feature requests, submit an issue on the Telescope GitHub repository: + +1. Go to [Telescope GitHub Issues](https://github.com/hyperweb-io/telescope/issues) +2. Click "New Issue" +3. Fill out the template with your information + +## Conclusion + +Troubleshooting issues in Telescope projects requires a systematic approach. By understanding the different types of errors you might encounter and following the debugging strategies outlined in this tutorial, you'll be better equipped to identify and resolve problems quickly. + +Remember that many issues are common and have been encountered by others before you. Don't hesitate to leverage the community resources available to help you overcome challenges in your Telescope projects. \ No newline at end of file diff --git a/learn/types.mdx b/learn/types.mdx new file mode 100644 index 0000000000..9e3860092b --- /dev/null +++ b/learn/types.mdx @@ -0,0 +1,390 @@ +# Working with Types + +In this tutorial, we'll dive into the TypeScript types generated by Telescope and learn how to use them effectively in your Cosmos SDK applications. Understanding these types is crucial for building type-safe applications that interact with the blockchain. + +## Understanding Generated Types + +When you run Telescope against Protocol Buffer definitions, it generates several TypeScript files containing types, interfaces, and helper functions. Let's explore the main categories of generated types and understand their purpose. + +### Message Types + +The most common types you'll work with are message types, which represent the data structures defined in Protocol Buffers. For example, a simple bank transfer message: + +```typescript +// In the original .proto file +message MsgSend { + string from_address = 1; + string to_address = 2; + repeated Coin amount = 3; +} +``` + +Telescope generates a TypeScript interface and helper functions: + +```typescript +// Generated TypeScript +export interface MsgSend { + fromAddress: string; + toAddress: string; + amount: Coin[]; +} + +export const MsgSend = { + // Serializes a MsgSend object to binary format + encode(message: MsgSend, writer: Writer = Writer.create()): Writer { + // implementation details... + }, + + // Deserializes binary data into a MsgSend object + decode(input: Reader | Uint8Array, length?: number): MsgSend { + // implementation details... + }, + + // Converts a JSON object to a MsgSend + fromJSON(object: any): MsgSend { + // implementation details... + }, + + // Converts a MsgSend to a JSON object + toJSON(message: MsgSend): unknown { + // implementation details... + }, + + // Creates a MsgSend from a partial object + fromPartial(object: DeepPartial): MsgSend { + // implementation details... + } +} +``` + +### Practical Example: Creating and Using a Message + +Let's see how to use these generated types in practice: + +```typescript +import { MsgSend } from "./cosmos/bank/v1beta1/tx"; + +// Create a message using the fromPartial helper +const transferMsg = MsgSend.fromPartial({ + fromAddress: "cosmos1sender...", + toAddress: "cosmos1recipient...", + amount: [ + { denom: "uatom", amount: "1000000" } // 1 ATOM (in microatoms) + ] +}); + +// You can also create the message directly +const anotherMsg: MsgSend = { + fromAddress: "cosmos1sender...", + toAddress: "cosmos1recipient...", + amount: [ + { denom: "uatom", amount: "1000000" } + ] +}; + +// Convert to JSON for debugging or other purposes +const jsonMsg = MsgSend.toJSON(transferMsg); +console.log(jsonMsg); + +// Later, you can recreate the message from JSON +const recreatedMsg = MsgSend.fromJSON(jsonMsg); +``` + +The `fromPartial` method is particularly useful as it allows you to create a message without providing all the fields, filling in defaults as appropriate. + +## Working with Any Types + +The Protocol Buffers `Any` type is a special container that can hold any message type. This is useful for scenarios where you need to handle multiple message types dynamically, such as in transaction processing. + +```typescript +import { Any } from "./google/protobuf/any"; +import { MsgSend } from "./cosmos/bank/v1beta1/tx"; +import { MsgDelegate } from "./cosmos/staking/v1beta1/tx"; + +// Create your message +const sendMsg = MsgSend.fromPartial({ + fromAddress: "cosmos1...", + toAddress: "cosmos1...", + amount: [{ denom: "uatom", amount: "1000000" }] +}); + +// Pack it into an Any +const packedMsg = Any.pack(sendMsg, "/cosmos.bank.v1beta1.MsgSend"); + +// Now you can include it in another message that expects Any types +// For example, in a transaction that can contain different message types + +// Later, you can unpack it when you know what type it contains +if (packedMsg.typeUrl === "/cosmos.bank.v1beta1.MsgSend") { + const unpackedSendMsg = packedMsg.unpack(MsgSend); + console.log(unpackedSendMsg.toAddress); +} else if (packedMsg.typeUrl === "/cosmos.staking.v1beta1.MsgDelegate") { + const unpackedDelegateMsg = packedMsg.unpack(MsgDelegate); + // Work with delegate message +} +``` + +### Using the Global Decoder Registry + +For more complex applications, Telescope can generate a global decoder registry to handle Any types more dynamically: + +```typescript +import { Registry } from "./registry"; +import { Any } from "./google/protobuf/any"; + +// Create a registry with all your types +const registry = new Registry(); + +// Any message can be unpacked using the registry +function processAnyMessage(anyMsg: Any) { + const unpacked = registry.unpack(anyMsg); + if (unpacked) { + // Handle the message based on its type + if ("toAddress" in unpacked) { + // This is likely a MsgSend + console.log(`Sending to: ${unpacked.toAddress}`); + } else if ("validatorAddress" in unpacked) { + // This is likely a MsgDelegate + console.log(`Delegating to: ${unpacked.validatorAddress}`); + } + } +} +``` + +## Amino Types for Legacy Compatibility + +If you're working with older wallets (like Keplr) or systems that use the Amino encoding format, you'll need to use the Amino types generated by Telescope. + +```typescript +import { MsgSend } from "./cosmos/bank/v1beta1/tx"; +import { AminoTypes } from "@cosmjs/stargate"; +import { aminoConverters } from "./amino/cosmos.bank.v1beta1.tx"; + +// Create your message +const sendMsg = MsgSend.fromPartial({ + fromAddress: "cosmos1...", + toAddress: "cosmos1...", + amount: [{ denom: "uatom", amount: "1000000" }] +}); + +// Set up amino types with the generated converters +const aminoTypes = new AminoTypes(aminoConverters); + +// Convert to amino format (for legacy wallets) +const aminoMsg = aminoTypes.toAmino({ + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: sendMsg +}); + +console.log(aminoMsg); +// Output: +// { +// type: "cosmos-sdk/MsgSend", +// value: { +// from_address: "cosmos1...", +// to_address: "cosmos1...", +// amount: [{ denom: "uatom", amount: "1000000" }] +// } +// } + +// Later, convert back from amino format +const protoMsg = aminoTypes.fromAmino(aminoMsg); +``` + +## Special Types and Their Handling + +Telescope provides special handling for certain common types: + +### Timestamps + +```typescript +import { Timestamp } from "./google/protobuf/timestamp"; + +// Create a timestamp from a Date +const now = new Date(); +const timestamp = Timestamp.fromDate(now); + +// Or manually +const manualTimestamp = Timestamp.fromPartial({ + seconds: BigInt(Math.floor(now.getTime() / 1000)), + nanos: (now.getTime() % 1000) * 1000000 +}); + +// Convert back to a Date +const dateFromTimestamp = timestamp.toDate(); +``` + +### Coins + +Cosmos SDK uses a special `Coin` type for representing tokens: + +```typescript +import { Coin } from "./cosmos/base/v1beta1/coin"; + +// Create a coin representing 5 ATOM +const atomCoin: Coin = { + denom: "uatom", + amount: "5000000" // 5 ATOM in microatoms (uatom) +}; + +// Helper function to format coins for display +function formatCoin(coin: Coin): string { + const amount = parseInt(coin.amount); + if (coin.denom.startsWith("u")) { + // Convert micro units to whole units for display + return `${amount / 1000000} ${coin.denom.substring(1).toUpperCase()}`; + } + return `${amount} ${coin.denom.toUpperCase()}`; +} + +console.log(formatCoin(atomCoin)); // "5 ATOM" +``` + +## Client Types and How to Use Them + +Telescope generates different client types to interact with the blockchain: + +### Query Clients + +Query clients are used to query the blockchain state: + +```typescript +import { createRPCQueryClient } from "./rpc.query"; + +// Create a query client connected to your node +const client = await createRPCQueryClient({ rpcEndpoint: "https://rpc.cosmos.network" }); + +// Query account balances +const balances = await client.cosmos.bank.v1beta1.allBalances({ + address: "cosmos1..." +}); + +console.log("Balances:", balances.balances); + +// Query validator information +const validator = await client.cosmos.staking.v1beta1.validator({ + validatorAddr: "cosmosvaloper1..." +}); + +console.log("Validator:", validator.validator); +``` + +### Transaction Clients + +Transaction clients are used to create and send transactions: + +```typescript +import { MsgSend } from "./cosmos/bank/v1beta1/tx"; +import { createTxClient } from "./tx"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; + +// Create a wallet +const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + "your mnemonic phrase here", + { prefix: "cosmos" } +); +const [account] = await wallet.getAccounts(); + +// Create a transaction client +const txClient = await createTxClient({ + signer: wallet, + rpcEndpoint: "https://rpc.cosmos.network" +}); + +// Create and send a transaction +const result = await txClient.signAndBroadcast( + account.address, + [ + MsgSend.fromPartial({ + fromAddress: account.address, + toAddress: "cosmos1recipient...", + amount: [{ denom: "uatom", amount: "1000000" }] + }) + ], + { + gas: "200000", + amount: [{ denom: "uatom", amount: "5000" }] + } +); + +console.log("Transaction result:", result); +``` + +## Type Safety Tips and Best Practices + +1. **Use `fromPartial` for creating messages** + + The `fromPartial` method provides a safer way to create messages by filling in default values for omitted fields: + + ```typescript + // Good: Uses fromPartial + const msg = MsgSend.fromPartial({ + fromAddress: "cosmos1...", + toAddress: "cosmos1..." + // amount will be initialized to an empty array + }); + + // Less safe: Direct initialization might leave required fields undefined + const directMsg: MsgSend = { + fromAddress: "cosmos1...", + toAddress: "cosmos1..." + // Oops, forgot amount! + }; + ``` + +2. **Leverage TypeScript's type checking** + + Let the TypeScript compiler help you catch errors: + + ```typescript + // TypeScript will flag this error + const msg = MsgSend.fromPartial({ + fromAddress: "cosmos1...", + toAddress: "cosmos1...", + amount: [{ denom: 123, amount: "1000" }] // Error: denom should be a string! + }); + ``` + +3. **Handle optional fields appropriately** + + ```typescript + function processProposal(proposal: any) { + // Check if optional fields exist before using them + if (proposal.content && proposal.content.title) { + console.log("Proposal title:", proposal.content.title); + } + + // Or use optional chaining + console.log("Proposal title:", proposal.content?.title); + } + ``` + +4. **Use typed arrays for repeated fields** + + ```typescript + // Create a properly typed array of coins + const amounts: Coin[] = [ + { denom: "uatom", amount: "1000000" }, + { denom: "usomm", amount: "5000000" } + ]; + + // Now you can use array methods with proper typing + const totalAmount = amounts + .filter(coin => coin.denom === "uatom") + .reduce((sum, coin) => sum + parseInt(coin.amount), 0); + ``` + +## Summary + +In this tutorial, we've explored the TypeScript types generated by Telescope and how to use them effectively in your applications. You've learned: + +- How to work with message types and their helper functions +- How to handle dynamic types with `Any` +- How to use Amino types for legacy compatibility +- How to work with special types like timestamps and coins +- How to use query and transaction clients to interact with the blockchain +- Best practices for maintaining type safety + +By leveraging these generated types, you can build more robust and type-safe applications that interact with Cosmos-based blockchains, with much less manual type definition and manipulation. + +Next, check out the [Composing Messages tutorial](./composing-messages.md) to learn how to create and send complex blockchain transactions. \ No newline at end of file diff --git a/packages/telescope/README.md b/packages/telescope/README.md index ff2f89bd22..42180cc6a5 100644 --- a/packages/telescope/README.md +++ b/packages/telescope/README.md @@ -109,179 +109,171 @@ The following blockchain libraries (generated by Telescope) are available via np ## Quickstart -Follow the instructions below to generate a new Typescript package that you can publish to npm. - -First, install `telescope` and `create-cosmos-app` +Follow the instructions below to generate a new Typescript package that you can publish to npm. You can also follow the video: https://youtu.be/iQf6p65fbdY +`create-interchain-app` ```sh -npm install -g @cosmology/telescope create-cosmos-app +npm install -g create-interchain-app ``` + ### Generate -Use the [`create-cosmos-app`](https://github.com/hyperweb-io/create-cosmos-app/) command to create a new package from the `telescope` boilerplate. +Use the [`create-interchain-app`](https://github.com/hyperweb-io/create-interchain-app/) command to create a new package from the `telescope` boilerplate. ```sh -cca --boilerplate telescope -``` +cia --boilerplate telescope -Then, you'll navigate into `./your-project/packages/telescope` package for the next steps. -You can also use `telescope generate` command to generate package according to the prompt or terminal command params such as: -`telescope generate --access public --userfullname testname --useremail test@gmail.com --module-desc test --username salkfl --license MIT --module-name test --chain-name cosmos --use-npm-scoped` - -The available options are: -`--userfullname` `--useremail` `--module-desc` `--username` `--module-name` `--chain-name` `--access` `--use-npm-scoped` `--license` +Then, you'll navigate into `./your-project/packages/telescope` package for the next steps. If some required options are missing, it will prompt to ask for the info. -To be noted, `--use-npm-scoped` only works when `--access` is `public` +For detailed cli `generate` commands, please check our docs and learn directories. -### Download protos with CLI +### Download protos The old ` telescope install ` command has been deprecated -You can use our CLI to download protos by using `download` command. +You can use our script to download protos by using `download-protos`command in the boilerplate. ```sh -telescope download +yarn download-protos ``` You should now see some repos cloned in `./git-modules` and proto files generated in `./protos`. These are the proto files downloaded according to your config. -Examples: +For detailed cli `download` commands, please check our docs and learn directories. -```sh -# Telescope will do the download according to .json file of --config -# Telescope will put proto into location specified by --out -telescope download --config ./protod.config.json --out ./git-modules -``` +### Transpile + +To create the Typescript files for your chain, run the `yarn codegen` command inside of the package. -```sh -# Telescope download from target repo according to --git-repo -# in format of (i.e. / or //) -# can be empty, it will use main as default -# Also --targets is required to specify the targets to download -# in format like cosmos/auth/v1beta1/auth.proto -telescope download --git-repo target-repo --targets target-proto ``` +yarn codegen +``` + +### Build + +Finally, run `install` and `build` to generate the JS and types for publishing your module to npm. ```sh -# ssh arg is optional, default is false -telescope download --config ./protod.config.json --out ./git-modules --ssh true +yarn build ``` +### Publishing -```js -// .protod.config.json example -// -// `repos` are the repository it's going to clone -// in format of (i.e. { "owner": , "repo": } or { "owner": , "repo": , "branch": }) -// can be empty, it will use main as default -// -// `protoDirMapping` is the directory of repo specified if the proto is not under repo's root directory ./protos -// in format of (i.e. / or //) -// can be empty, it will use main as default -// -// `outDir` is where the output proto will be put -// -// `targets` are the target proto to download -// `targets` can be patterns like: -// "cosmos/bank/v1beta1/tx.proto", -// "cosmos/gov/**/*.proto", -// "cosmos/authz/**/*.proto", -{ - "repos": [ - { "owner": "cosmos", "repo": "cosmos-sdk" }, - ... - ], - "protoDirMapping": { - "gogo/protobuf/master": ".", - ... - }, - "outDir": "protos", - "ssh": true, - "tempRepoDir": "git-modules", - "targets": [ - "cosmos/auth/v1beta1/auth.proto", - ... - ] -} -``` +Now you should have code inside of your `./src` folder, ready for publshing. If you used the `create-interchain-app` boilerplate, use `lerna` to publish (and/or read the README in the boilerplate for instructions), or run `npm publish` from your repository. -### Transpile +# Usage -To create the Typescript files for your chain, run the `yarn codegen` command inside of the package. +## Advanced Install +The methods below are all the options you can use to install and use Telescope +### Telescope CLI +Install telescope +```sh +npm install -g @cosmology/telescope ``` -yarn codegen -``` +The steps by order are: generate, download and transpile. -### Transpile with CLI +1.Generate a package with the telescope CLI: -Less recommended, but you can also use our CLI for transpilation. To create the Typescript files with the `cli`, run the `transpile` command. +Use and follow the default prompt: +```sh +telescope generate +``` +Or advanced cli option by your choice: ```sh -telescope transpile +telescope generate --access public --userfullname "Your Name" --useremail "your@email.com" --module-desc "Your module description" --username "your-username" --license MIT --module-name "your-module" --chain-name cosmos --use-npm-scoped ``` -You should now see some `.ts` files generated in `./src`. These are the real source files used in your application. +Available options: +- `--userfullname`: Your full name +- `--useremail`: Your email +- `--module-desc`: Module description +- `--username`: GitHub username +- `--module-name`: Module name +- `--chain-name`: Chain name +- `--access`: Package access (`public` or `private`) +- `--use-npm-scoped`: Use npm scoped package (only works with `--access public`) +- `--license`: License type + +2.Download protocol buffer files: +```sh +telescope download +``` -Examples: +This will clone repositories into `./git-modules` and generate proto files in `./protos`. +Download with a config file: ```sh -# Telescope takes chain1 folder as input, -# and generate files in 'gen/src' folder. -telescope transpile --protoDirs ../../__fixtures__/chain1 --outPath gen/src +telescope download --config ./protod.config.json --out ./git-modules ``` +Download from a specific repo: ```sh -# Telescope takes chain1 folder as input, -# and generate files in 'gen/src' folder using default telescope options. -telescope transpile --protoDirs ../../__fixtures__/chain1 --outPath gen/src --useDefaults +telescope download --git-repo owner/repository --targets cosmos/auth/v1beta1/auth.proto ``` +3. Transpile (Generate TypeScript code from proto files): +Use default telescope option: ```sh -# Telescope takes chain1 folder(from args) and chain2 folder(from config) as input, -# and generate files in 'gen/src'(defined in the config file, will override outPath in args) folder using a config file. -# Note: --config will override --useDefaults. -telescope transpile --protoDirs ../../__fixtures__/chain1 --config .telescope.json +telescope transpile ``` +Use customized telescope option: ```sh -# Telescope takes more than one config. The config afterward will override those in front. In this case values in .telescope-ext.json will override those in .telescope.json. -telescope transpile --config .telescope.json --config .telescope-ext.json +telescope transpile --config your-config.json ``` -```js -//.telescope.json -{ - "protoDirs": [ - "../../fixtures/chain2" - ], - "outPath": "gen/src", - "options": { - // telescope options - ... - } -} +### CIA +Please follow the video: https://youtu.be/iQf6p65fbdY +Or [Go to Quickstart](#quickstart) + +### CCA +First, install `create-cosmos-app` + +```sh +npm install -g create-cosmos-app ``` -### Build +Use the [`create-cosmos-app`](https://github.com/hyperweb-io/create-cosmos-app/) command to create a new package from the `telescope` boilerplate. -Finally, run `install` and `build` to generate the JS and types for publishing your module to npm. +```sh +cca --boilerplate telescope +``` +Then, you'll navigate into `./your-project/packages/telescope` package for the next steps. + +Install dependency and use cli to download the protos you want. ```sh -yarn build +yarn install +telescope download --config ./your.config.json ``` -### Publishing +To create the Typescript files for your chain, run the `yarn codegen` command inside of the package. -Now you should have code inside of your `./src` folder, ready for publshing. If you used the `create-cosmos-app` boilerplate, use `lerna` to publish (and/or read the README in the boilerplate for instructions), or run `npm publish` from your repository. +```sh +yarn codegen +``` -# Usage +### Manual install +If you want to use telescope in your own project. -## Programatic Usage +Run the command in ./your-project +```sh +yarn add --dev @cosmology/telescope +``` +Install helpers and cosmjs [dependencies listed here](#dependencies) + +We recommand to use [Go to Programatic Usage](#programatic-usage) + +You can also use [Go to Telescope Cli](#telescope-cli) +To be noted for cli command, add 'npx' or 'yarn' prefix when you use it within your project. For instance: 'yarn telescope generate', 'npx telescope download', etc. + +### Programatic Usage First add telescope to your `devDependencies`: @@ -291,6 +283,39 @@ yarn add --dev @cosmology/telescope Install helpers and cosmjs [dependencies listed here](#dependencies) +Download command example: +```js +import downloadProtos from '@cosmology/telescope/main/commands/download' + +const config = { + repos: [ + { owner: "cosmos", repo: "cosmos-sdk", branch: "release/v0.50.x" }, + { owner: "cosmos", repo: "ibc-go" }, + ], + protoDirMapping: { + "gogo/protobuf/master": ".", + "googleapis/googleapis/master": ".", + "protocolbuffers/protobuf/main": "src" + }, + outDir: "protos", + ssh: false, + tempRepoDir: "git-modules", + targets: [ + "cosmos/**/*.proto", + "ibc/**/*.proto", + ] +}; + +downloadProtos(config) + .then(() => console.log('✅ Proto download completed')) + // @ts-ignore + .catch((error) => { + console.error('❌ Proto download failed:', error); + process.exit(1); + }); +``` + +Transpile command example: ```js import { join } from 'path'; import telescope from '@cosmology/telescope'; @@ -1365,7 +1390,7 @@ A unified toolkit for building applications and smart contracts in the Interchai | **Wallet Connectors**| [**Interchain Kit**](https://github.com/hyperweb-io/interchain-kit)beta, [**Cosmos Kit**](https://github.com/hyperweb-io/cosmos-kit) | Experience the convenience of connecting with a variety of web3 wallets through a single, streamlined interface. | | **Signing Clients** | [**InterchainJS**](https://github.com/hyperweb-io/interchainjs)beta, [**CosmJS**](https://github.com/cosmos/cosmjs) | A single, universal signing interface for any network | | **SDK Clients** | [**Telescope**](https://github.com/hyperweb-io/telescope) | Your Frontend Companion for Building with TypeScript with Cosmos SDK Modules. | -| **Starter Kits** | [**Create Interchain App**](https://github.com/hyperweb-io/create-interchain-app)beta, [**Create Cosmos App**](https://github.com/hyperweb-io/create-cosmos-app) | Set up a modern Interchain app by running one command. | +| **Starter Kits** | [**Create Interchain App**](https://github.com/hyperweb-io/create-interchain-app)beta | Set up a modern Interchain app by running one command. | | **UI Kits** | [**Interchain UI**](https://github.com/hyperweb-io/interchain-ui) | The Interchain Design System, empowering developers with a flexible, easy-to-use UI kit. | | **Testing Frameworks** | [**Starship**](https://github.com/hyperweb-io/starship) | Unified Testing and Development for the Interchain. | | **TypeScript Smart Contracts** | [**Create Hyperweb App**](https://github.com/hyperweb-io/create-hyperweb-app) | Build and deploy full-stack blockchain applications with TypeScript | @@ -1388,4 +1413,4 @@ Thanks to these engineers, teams and projects for inspiring Telescope: AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. -No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value. \ No newline at end of file +No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.