Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
- [#4595](https://github.com/ignite/cli/pull/4595) Fix wrong InterfaceRegistry for IBC modules.
- [#4609](https://github.com/ignite/cli/pull/4609) Add work dir for relayer integration tests.

### Bug Fixes

- [#4347](https://github.com/ignite/cli/pull/4347) Fix `ts-client` generation

## [`v29.0.0-beta.1`](https://github.com/ignite/cli/releases/tag/v29.0.0-beta.1)

### Features
Expand Down
5 changes: 0 additions & 5 deletions docs/docs/04-clients/02-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ description: Information about the generated TypeScript client code.

# TypeScript frontend

:::warning
The TypeScript client is being reworked and is not yet stable.
In the meantime, refer to the [Ignite CCA App](https://ignite.com/marketplace/cca).
:::

Ignite offers powerful functionality for generating client-side code for your
blockchain. Think of this as a one-click client SDK generation tailored
specifically for your blockchain.
Expand Down
7 changes: 3 additions & 4 deletions ignite/cmd/generate_composables.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import (

func NewGenerateComposables() *cobra.Command {
c := &cobra.Command{
Hidden: true, // hidden util we have a better ts-client.
Use: "composables",
Short: "TypeScript frontend client and Vue 3 composables",
RunE: generateComposablesHandler,
Use: "composables",
Short: "TypeScript frontend client and Vue 3 composables",
RunE: generateComposablesHandler,
}

c.Flags().AddFlagSet(flagSetYes())
Expand Down
7 changes: 3 additions & 4 deletions ignite/cmd/generate_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import (

func NewGenerateHooks() *cobra.Command {
c := &cobra.Command{
Hidden: true, // hidden util we have a better ts-client.
Use: "hooks",
Short: "TypeScript frontend client and React hooks",
RunE: generateHooksHandler,
Use: "hooks",
Short: "TypeScript frontend client and React hooks",
RunE: generateHooksHandler,
}

c.Flags().AddFlagSet(flagSetYes())
Expand Down
19 changes: 10 additions & 9 deletions ignite/cmd/generate_typescript_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@ import (

const (
flagUseCache = "use-cache"
msgBufAuth = "Generate ts-client depends on a 'buf.build' remote plugin, and as of August 1, 2024, Buf will begin limiting remote plugin requests from unauthenticated users on 'buf.build'. If you send more than ten unauthenticated requests per hour using remote plugins, you’ll start to see rate limit errors. Please authenticate before running ts-client command using 'buf registry login' command and follow the instructions. For more info, check https://buf.build/docs/generate/auth-required."
msgBufAuth = "Generate ts-client uses a 'buf.build' remote plugin. Buf is begin limiting remote plugin requests from unauthenticated users on 'buf.build'. Intensively using this function will get you rate limited. Authenticate with 'buf registry login' to avoid this (https://buf.build/docs/generate/auth-required)."
)

func NewGenerateTSClient() *cobra.Command {
c := &cobra.Command{
Hidden: true, // hidden util we have a better ts-client.
Use: "ts-client",
Short: "TypeScript frontend client",
Use: "ts-client",
Short: "TypeScript frontend client",
Long: `Generate a framework agnostic TypeScript client for your blockchain project.

By default the TypeScript client is generated in the "ts-client/" directory. You
Expand Down Expand Up @@ -51,12 +50,14 @@ func generateTSClientHandler(cmd *cobra.Command, _ []string) error {
session := cliui.New(cliui.StartSpinnerWithText(statusGenerating))
defer session.End()

if err := session.AskConfirm(msgBufAuth); err != nil {
if errors.Is(err, cliui.ErrAbort) {
return errors.New("buf not auth")
}
if !getYes(cmd) {
if err := session.AskConfirm(msgBufAuth); err != nil {
if errors.Is(err, cliui.ErrAbort) {
return errors.New("buf not auth")
}

return err
return err
}
}

c, err := chain.NewWithHomeFlags(
Expand Down
9 changes: 1 addition & 8 deletions ignite/pkg/cosmosanalysis/module/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ type moduleDiscoverer struct {
// IsCosmosSDKModulePkg check if a Go import path is a Cosmos SDK package module.
// These type of package have the "cosmossdk.io/x" prefix.
func IsCosmosSDKModulePkg(path string) bool {
return strings.Contains(path, "cosmossdk.io/x/")
return strings.Contains(path, "cosmossdk.io/x/") || strings.Contains(path, "github.com/cosmos/cosmos-sdk")
}

// Discover discovers and returns modules and their types that are registered in the app
Expand Down Expand Up @@ -280,13 +280,6 @@ func (d *moduleDiscoverer) discover(pkg protoanalysis.Package) (Module, error) {
return false
}

// do not use if an SDK message.
for _, msg := range msgs {
if msg == protomsg.Name {
return false
}
}

// do not use if used as a request/return type of RPC.
for _, s := range pkg.Services {
for _, q := range s.RPCFuncs {
Expand Down
99 changes: 50 additions & 49 deletions ignite/pkg/cosmosgen/generate_typescript.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ type tsGenerator struct {
}

type generatePayload struct {
Modules []module.Module
PackageNS string
IsConsumerChain bool
Modules []module.Module
PackageNS string
}

func newTSGenerator(g *generator) *tsGenerator {
Expand All @@ -44,23 +43,10 @@ func (g *generator) generateTS(ctx context.Context) error {

appModulePath := gomodulepath.ExtractAppPath(chainPath.RawPath)
data := generatePayload{
Modules: g.appModules,
PackageNS: strings.ReplaceAll(appModulePath, "/", "-"),
IsConsumerChain: false,
Modules: g.appModules,
PackageNS: strings.ReplaceAll(appModulePath, "/", "-"),
}

// Third party modules are always required to generate the root
// template because otherwise it would be generated only with
// custom modules losing the registration of the third party
// modules when the root templates are re-generated.
for _, modules := range g.thirdModules {
data.Modules = append(data.Modules, modules...)
for _, m := range modules {
if strings.HasPrefix(m.Pkg.Name, "interchain_security.ccv.consumer") {
data.IsConsumerChain = true
}
}
}
// Make sure the modules are always sorted to keep the import
// and module registration order consistent so the generated
// files are not changed.
Expand All @@ -77,46 +63,50 @@ func (g *generator) generateTS(ctx context.Context) error {
}

func (g *tsGenerator) generateModuleTemplates(ctx context.Context) error {
gg := &errgroup.Group{}
dirCache := cache.New[[]byte](g.g.cacheStorage, dirchangeCacheNamespace)
add := func(sourcePath string, modules []module.Module) {
for _, m := range modules {
gg.Go(func() error {
cacheKey := m.Pkg.Path
paths := []string{m.Pkg.Path, g.g.opts.jsOut(m)}

// Always generate module templates by default unless cache is enabled, in which
// case the module template is generated when one or more files were changed in
// the module since the last generation.
if g.g.opts.useCache {
changed, err := dirchange.HasDirChecksumChanged(dirCache, cacheKey, sourcePath, paths...)
if err != nil {
return err
}

if !changed {
return nil
}
}

if err := g.generateModuleTemplate(ctx, sourcePath, m); err != nil {
return err
}

return dirchange.SaveDirChecksum(dirCache, cacheKey, sourcePath, paths...)
})
add := func(sourcePath string, m module.Module) error {
cacheKey := m.Pkg.Path
paths := []string{m.Pkg.Path, g.g.opts.jsOut(m)}

// Always generate module templates by default unless cache is enabled, in which
// case the module template is generated when one or more files were changed in
// the module since the last generation.
if g.g.opts.useCache {
changed, err := dirchange.HasDirChecksumChanged(dirCache, cacheKey, sourcePath, paths...)
if err != nil {
return err
}

if !changed {
return nil
}
}

if err := g.generateModuleTemplate(ctx, sourcePath, m); err != nil {
return err
}

return dirchange.SaveDirChecksum(dirCache, cacheKey, sourcePath, paths...)
}

add(g.g.appPath, g.g.appModules)
gg := &errgroup.Group{}
for _, m := range g.g.appModules {
gg.Go(func() error {
return add(g.g.appPath, m)
})
}

// Always generate third party modules; This is required because not generating them might
// lead to issues with the module registration in the root template. The root template must
// always be generated with 3rd party modules which means that if a new 3rd party module
// is available and not generated it would lead to the registration of a new not generated
// 3rd party module.
for sourcePath, modules := range g.g.thirdModules {
add(sourcePath, modules)
for _, m := range modules {
gg.Go(func() error {
return add(sourcePath, m)
})
}
}

return gg.Wait()
Expand All @@ -131,6 +121,7 @@ func (g *tsGenerator) generateModuleTemplate(
out = g.g.opts.jsOut(m)
typesOut = filepath.Join(out, "types")
)

if err := os.MkdirAll(typesOut, 0o766); err != nil {
return err
}
Expand All @@ -140,7 +131,7 @@ func (g *tsGenerator) generateModuleTemplate(

// All "cosmossdk.io" module packages must use SDK's
// proto path which is where the proto files are stored.
protoPath := filepath.Join(g.g.appPath, g.g.protoDir)
protoPath := filepath.Join(appPath, g.g.protoDir) // use module app path
if module.IsCosmosSDKModulePkg(appPath) {
protoPath = filepath.Join(g.g.sdkDir, "proto")
}
Expand All @@ -156,7 +147,17 @@ func (g *tsGenerator) generateModuleTemplate(
return err
}

return templateTSClientModule.Write(out, protoPath, struct {
// Generate the module template
if err := templateTSClientModule.Write(out, protoPath, struct {
Module module.Module
}{
Module: m,
}); err != nil {
return err
}

// Generate the rest API template (using axios)
return templateTSClientRest.Write(out, protoPath, struct {
Module module.Module
}{
Module: m,
Expand Down
1 change: 1 addition & 0 deletions ignite/pkg/cosmosgen/generate_typescript_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package cosmosgen_test
11 changes: 8 additions & 3 deletions ignite/pkg/cosmosgen/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var (

templateTSClientRoot = newTemplateWriter("root")
templateTSClientModule = newTemplateWriter("module")
templateTSClientRest = newTemplateWriter("rest")
templateTSClientComposable = newTemplateWriter("composable")
templateTSClientComposableRoot = newTemplateWriter("composable-root")
)
Expand Down Expand Up @@ -68,10 +69,14 @@ func (t templateWriter) Write(destDir, protoPath string, data interface{}) error

return xstrcase.UpperCamel(replacer.Replace(word))
},
"resolveFile": func(fullPath string) string { // TODO to check
rel, _ := filepath.Rel(protoPath, fullPath)
"resolveFile": func(fullPath string) string {
_ = protoPath // eventually, we should use the proto folder name of this, for the application (but not for the other modules)

res := strings.Split(fullPath, "proto/")
rel := res[len(res)-1] // get path after proto/
rel = strings.TrimSuffix(rel, ".proto")
return rel

return "./types/" + rel
},
"inc": func(i int) int {
return i + 1
Expand Down
2 changes: 1 addition & 1 deletion ignite/pkg/cosmosgen/templates/module/module.ts.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { msgTypes } from './registry';
import { IgniteClient } from "../client"
import { MissingWalletError } from "../helpers"
import { Api } from "./rest";
{{ range .Module.Msgs }}import { {{ .Name }} } from "./types/{{ resolveFile .FilePath }}";
{{ range .Module.Msgs }}import { {{ .Name }} } from "{{ resolveFile .FilePath }}";
{{ end }}
{{ range .Module.Types }}import { {{ .Name }} as type{{- .Name -}} } from "./types"
{{ end }}
Expand Down
2 changes: 1 addition & 1 deletion ignite/pkg/cosmosgen/templates/module/registry.ts.tpl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GeneratedType } from "@cosmjs/proto-signing";
{{ range .Module.Msgs }}import { {{ .Name }} } from "./types/{{ resolveFile .FilePath }}";
{{ range .Module.Msgs }}import { {{ .Name }} } from "{{ resolveFile .FilePath }}";
{{ end }}
const msgTypes: Array<[string, GeneratedType]> = [
{{ range .Module.Msgs }}["/{{ .URI }}", {{ .Name }}],
Expand Down
2 changes: 1 addition & 1 deletion ignite/pkg/cosmosgen/templates/module/types.ts.tpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{ range .Module.Types }}import { {{ .Name }} } from "./types/{{ resolveFile .FilePath }}"
{{ range .Module.Types }}import { {{ .Name }} } from "{{ resolveFile .FilePath }}"
{{ end }}

export {
Expand Down
45 changes: 45 additions & 0 deletions ignite/pkg/cosmosgen/templates/rest/rest.ts.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* eslint-disable */
import axios from 'axios'
import * as qs from 'qs'

// this is used to derive the proper return types for query endpoints
export type BaseQueryClient = {
queryBalances: (address: string, params?: any) => Promise<any>
}

export class Api {
private axios: any
private baseURL: string

constructor({ baseURL }: { baseURL: string }) {
this.baseURL = baseURL
this.axios = axios.create({
baseURL,
timeout: 30000,
paramsSerializer: function(params: any) {
return qs.stringify(params, { arrayFormat: 'repeat' })
}
})
}

// common helper for most simple operations
private async handleRequest(url: string, params?: any): Promise<any> {
try {
const response = await this.axios.get(url, { params })
return response
} catch (e: any) {
if (e.response?.data) {
console.error('Error in API request:', e.response.data)
}
throw e
}
}

// Return URL for specific module endpoints
public getModuleEndpoint(endpoint: string): string {
return `${this.baseURL}/${endpoint}`
}

// Methods for specific module endpoints can be added here
// The actual methods will be auto-generated from OpenAPI specs in a real implementation
}
7 changes: 1 addition & 6 deletions ignite/pkg/cosmosgen/templates/root/client.ts.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,9 @@ export class IgniteClient extends EventEmitter {
).queryClient;
const bankQueryClient = (await import("./cosmos.bank.v1beta1/module"))
.queryClient;
{{ if eq .IsConsumerChain false }}
const stakingQueryClient = (await import("./cosmos.staking.v1beta1/module")).queryClient;
const stakingqc = stakingQueryClient({ addr: this.env.apiURL });
const staking = await (await stakingqc.queryParams()).data;
{{ end }}
const qc = queryClient({ addr: this.env.apiURL });
const node_info = await (await qc.serviceGetNodeInfo()).data;
const chainId = node_info.default_node_info?.network ?? "";
Expand Down Expand Up @@ -116,15 +114,12 @@ export class IgniteClient extends EventEmitter {
return y;
}) ?? [];

{{ if eq .IsConsumerChain true -}}
let stakeCurrency = currencies.find((x) => !x.coinDenom.startsWith("ibc/"));
{{ else }}
let stakeCurrency = {
coinDenom: staking.params?.bond_denom?.toUpperCase() ?? "",
coinMinimalDenom: staking.params?.bond_denom ?? "",
coinDecimals: 0,
};
{{ end }}

let feeCurrencies =
tokens.supply?.map((x) => {
const y = {
Expand Down
2 changes: 1 addition & 1 deletion ignite/pkg/cosmosgen/templates/root/index.ts.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { Registry } from '@cosmjs/proto-signing'
import { IgniteClient } from "./client";
import { MissingWalletError } from "./helpers";
{{ range .Modules }}import { IgntModule as {{ camelCaseUpperSta .Pkg.Name }}, msgTypes as {{ camelCaseUpperSta .Pkg.Name }}MsgTypes } from './{{ .Pkg.Name }}'
{{ range .Modules }}import { IgntModule as {{ camelCaseUpperSta .Pkg.Name }}, msgTypes as {{ camelCaseUpperSta .Pkg.Name }}MsgTypes } from './{{ replace .Pkg.Name "." "/" }}'
{{ end }}

const Client = IgniteClient.plugin([
Expand Down