Skip to content
Open
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
7 changes: 6 additions & 1 deletion .github/workflows/buildBinaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ jobs:
cd "$GITHUB_WORKSPACE/proto"
buf generate --template buf.gen.ts.yaml
cd "$GITHUB_WORKSPACE/ts-proto"
PKG_SOURCE_VERSION=$(node -p "require('./package.json').version")
echo "Checked-in ts-proto package version: ${PKG_SOURCE_VERSION}"
if [ "${GITHUB_EVENT_NAME}" = "release" ] && [ "${PKG_SOURCE_VERSION}" != "${NPM_VERSION}" ]; then
echo "::warning:: ts-proto/package.json version (${PKG_SOURCE_VERSION}) differs from release tag version (${NPM_VERSION}). The release workflow will override it before packing."
fi
if [ -f package-lock.json ]; then
npm ci
else
Expand All @@ -127,7 +132,7 @@ jobs:
if: env.VERSION != 'dev' && github.event_name == 'release'
working-directory: ts-proto
continue-on-error: true
run: npm publish --provenance # Required for npm OIDC trusted publishing
run: npm publish --provenance --tag pr-${{ github.run_id }} # Temporary tag; does not move latest or next
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
run: npm publish --provenance --tag pr-${{ github.run_id }} # Temporary tag; does not move latest or next
run: npm publish --provenance --tag pr-${{ github.event.number }}

Apparently, github.event.number holds the number assigned to the PR it is running from. So the resulting tag would be pr-268 in this case. This would be IMHO a good solution, since the app consuming this PR will be usually interested in getting the last version released for it.

I didn't test it by myself but that's the approach we follow in Credo and shown in some examples on GitHub documentation.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@genaris Oh okay that's cool thanks, I didn't even know this was a thing. It's better to use this since it's unique but also predictable for us and doesn't change


- name: Build binary for Linux AMD64
run: |
Expand Down
8 changes: 4 additions & 4 deletions proto/buf.gen.ts.yaml

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're going to generate the proto libraries to be included in the verana-types version, you also need to specify the versions to be used. The issue indicated that the libraries from verana-front that correspond to the currently working version should be copied, which isn't the older version like the ones that were replaced. The idea isn't to generate them again, but to copy those that are known to correspond to the specified protoc version and that work correctly on the front end.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that if we generate these files, the tool and version used should be clear.

The real source of truth here is the proto files. The TypeScript codec files are only generated output from those proto definitions, so we should document the generation setup better so local generation, CI and release packaging all use the same recipe.

Where I disagree is with not generating them again. If the proto files are the source of truth, then the TypeScript package has to be generated from those proto files, not copied by hand from another repo.

This package belongs in verana because verana owns those proto definitions and needs to provide the serialization layer that matches them. It should not be up to each client to reimplement that logic separately from the same proto files.

And the frontend is not the only consumer here. The same package needs to be usable by the frontend, the indexer, the visualizer, and anything else talking to the chain.

What this branch does is bring that back into verana, and the frontend has already tested the published package successfully. That is better than keeping two copies in sync by hand.

Copy link

@antoniobenavidesvallejo antoniobenavidesvallejo Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are contradictions in what you've written. The goal of the verana-types library is for the amino converters and proto codecs to be packaged and used by the front end and any application that requires them. In other words, there will be no need for local generation; only the published library will be used.

Now, the point is that to maintain the library, there are two possibilities:

  1. The proto codecs are generated locally and tested, as are the adjustments that will likely need to be made to the Amino converters. All of this is then uploaded to generate a new version of the library with the required compatibility.
  2. The proto codecs are not uploaded to the repository but are instead generated in the processes that run when the library is created. In this case, it's necessary that these processes ensure the version of the protoc they use for generation is the same as the one used to make the amino converter adjustments. Currently protoc-gen-ts_proto v1.181.2 and protoc v5.29.3 (libprotoc 29.3), as indicated in the issue. In this case, the proto codecs would not be part of the PR; they would be generated during packaging.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is clearer.

I think this PR is following your option 1, not option 2.

In this repo today, the generated codec files are part of the repository, they are regenerated in CI, and they are tested together with the Amino layer before publishing. That is why the codec files are part of this PR.

I also agree that the Amino converters may need manual adjustments when proto changes affect signing behavior. That is exactly why I am separating the generated codec files from the handwritten Amino layer.

I don’t think option 2 is a good fit here. If the generated codec files only appear at packaging time, then the real published output is no longer fully visible in the PR itself. That makes it harder to review, harder to test end to end, and harder to know later whether a bug came from the proto changes, the generated codec output, the Amino layer, or the packaging step.

So for this PR, I think the practical answer is:

  • keep the proto files as the source of truth
  • keep the generated codec files in the repo
  • make the generation setup clearer
  • publish the tested package from that checked-in state

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ plugins:
- plugin: buf.build/community/stephenh-ts-proto:v1.181.2
out: ../ts-proto/src/codec
opt:
- ts_proto_opt=esModuleInterop=true
- ts_proto_opt=forceLong=long
- ts_proto_opt=useOptionals=messages
- ts_proto_opt=snakeToCamel=true
- esModuleInterop=true
- forceLong=long
- useOptionals=messages
- snakeToCamel=true
1 change: 1 addition & 0 deletions ts-proto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ See `ts-proto/test/README.md` for complete examples.

## Publish
1. Bump the version in `package.json` to match the chain tag you are releasing (e.g. `v0.7.0` -> `0.7.0`).
The release workflow stamps the published tarball version from the Git tag, but the checked-in `package.json` should still be kept aligned with the latest intended release version so local `npm pack` output and repo metadata stay honest.
2. Regenerate and build as above.
3. `npm publish` (or `npm pack` and upload the tarball to the GitHub release assets).

Expand Down
7 changes: 4 additions & 3 deletions ts-proto/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 28 additions & 4 deletions ts-proto/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@verana-labs/verana-types",
"version": "0.9.1",
"version": "0.9.4",
"description": "Generated TypeScript protobuf codecs for the Verana blockchain.",
"license": "AGPL-3.0-only",
"main": "dist/index.js",
Expand All @@ -9,8 +9,31 @@
"dist"
],
"exports": {
"./codec/*": "./dist/codec/*",
".": "./dist/index.js"
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"default": "./dist/index.js"
},
"./signing": {
"types": "./dist/signing.d.ts",
"require": "./dist/signing.js",
"default": "./dist/signing.js"
},
"./codec/*": {
"types": "./dist/codec/*.d.ts",
"require": "./dist/codec/*.js",
"default": "./dist/codec/*.js"
},
"./amino-converter/*": {
"types": "./dist/amino-converter/*.d.ts",
"require": "./dist/amino-converter/*.js",
"default": "./dist/amino-converter/*.js"
},
"./helpers/*": {
"types": "./dist/helpers/*.d.ts",
"require": "./dist/helpers/*.js",
"default": "./dist/helpers/*.js"
}
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
Expand All @@ -27,7 +50,8 @@
"typescript": "5.7.3"
},
"peerDependencies": {
"@cosmjs/stargate": "0.32.3"
"@cosmjs/proto-signing": "^0.32.3",
"@cosmjs/stargate": "^0.32.3"
},
"repository": {
"type": "git",
Expand Down
76 changes: 76 additions & 0 deletions ts-proto/src/amino-converter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Amino Converters

Amino converters translate between Protobuf messages and Amino JSON format, which is required for Amino (LEGACY_AMINO_JSON) signing with CosmJS.

## Structure

```
amino-converter/
cs.ts # Credential Schema module converters
dd.ts # DID Directory module converters
perm.ts # Permission module converters
td.ts # Trust Deposit module converters
tr.ts # Trust Registry module converters
util/
helpers.ts # Shared conversion helpers
```

## Helper Reference

When creating a new Amino converter, use the helpers from `./util/helpers` based on the field types in the Protobuf message:

| Proto Field Type | toAmino (Proto -> JSON) | fromAmino (JSON -> Proto) | Notes |
|---|---|---|---|
| `uint64` / `Long` | `u64ToStr(value)` | `strToU64(value)` | Amino encodes uint64 as string |
| `uint32` | `u32ToAmino(value)` | direct assignment | Preserves 0 as `0`, omits `null` |
| `OptionalUInt32` | `toOptU32Amino(value)` | `fromOptU32Amino(value)` | 0 -> `{}`, n -> `{value: n}`, absent -> `undefined` |
| `google.protobuf.Timestamp` / `Date` | `dateToIsoAmino(value)` | `isoToDate(value)` | ISO 8601 string, trims `.000Z` to `Z` |
| `string` | direct assignment | direct assignment | No conversion needed |
| `bool` | `value ? true : undefined` | `value ?? false` | Omit `false` for omitempty |
| `string` (optional) | `value ?? ''` | `value ?? ''` | Use empty string default |

### Additional helpers

- **`clean(obj)`**: Removes `undefined` fields from an object. Use when fields should be omitted (omitempty) rather than sent as `null`.
- **`pickOptionalUInt32(value)`**: Parses loosely-typed input (string, number) into an `OptionalUInt32` wrapper.

## Creating a New Amino Converter

1. Create a new file in `amino-converter/` named after the module (e.g., `mymodule.ts`).
2. Import the Protobuf message types from the codec: `../codec/verana/<module>/v1/tx`.
3. Import helpers from `./util/helpers`.
4. For each message type, export a converter object with:
- `aminoType`: the full proto type URL (e.g., `'/verana.mymodule.v1.MsgDoSomething'`)
- `toAmino(msg)`: converts Proto message to Amino JSON object
- `fromAmino(value)`: converts Amino JSON back to Proto message using `Msg.fromPartial()`

### Field naming convention

- Proto uses **camelCase** (e.g., `schemaId`, `docUrl`)
- Amino JSON uses **snake_case** (e.g., `schema_id`, `doc_url`)

### Example

```typescript
import { MsgDoSomething } from '../codec/verana/mymodule/v1/tx';
import { u64ToStr, strToU64, clean } from './util/helpers';

export const MsgDoSomethingAminoConverter = {
aminoType: '/verana.mymodule.v1.MsgDoSomething',
toAmino: (msg: MsgDoSomething) => clean({
creator: msg.creator ?? '',
some_id: u64ToStr(msg.someId), // uint64 -> string
}),
fromAmino: (value: any) =>
MsgDoSomething.fromPartial({
creator: value.creator ?? '',
someId: strToU64(value.some_id), // string -> uint64
}),
};
```

## Proto Codecs

Generated with:
- `protoc-gen-ts_proto` v1.181.2
- `protoc` v5.29.3 (libprotoc 29.3)
87 changes: 87 additions & 0 deletions ts-proto/src/amino-converter/cs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use client';

import {
MsgCreateCredentialSchema,
MsgUpdateCredentialSchema,
MsgArchiveCredentialSchema,
} from '../codec/verana/cs/v1/tx';
import { u64ToStr, strToU64, u32ToAmino, fromOptU32Amino, toOptU32Amino, clean } from './util/helpers';

/**
* Amino converter for MsgCreateCredentialSchema
*/
export const MsgCreateCredentialSchemaAminoConverter = {
aminoType: '/verana.cs.v1.MsgCreateCredentialSchema',
// Proto → Amino JSON
toAmino: (msg: MsgCreateCredentialSchema) => clean({
creator: msg.creator ?? '',
tr_id: u64ToStr(msg.trId), // uint64 -> string
json_schema: msg.jsonSchema ?? '',
issuer_grantor_validation_validity_period: toOptU32Amino(msg.issuerGrantorValidationValidityPeriod),
verifier_grantor_validation_validity_period: toOptU32Amino(msg.verifierGrantorValidationValidityPeriod),
issuer_validation_validity_period: toOptU32Amino(msg.issuerValidationValidityPeriod),
verifier_validation_validity_period: toOptU32Amino(msg.verifierValidationValidityPeriod),
holder_validation_validity_period: toOptU32Amino(msg.holderValidationValidityPeriod),
issuer_perm_management_mode: u32ToAmino(msg.issuerPermManagementMode) , // uint32 -> number
verifier_perm_management_mode: u32ToAmino(msg.verifierPermManagementMode) , // uint32 -> number
}),
// Amino JSON → Proto
fromAmino: (value: any ): MsgCreateCredentialSchema =>
MsgCreateCredentialSchema.fromPartial({
creator: value.creator ?? '',
trId: strToU64(value.tr_id),
jsonSchema: value.json_schema ?? '',
issuerGrantorValidationValidityPeriod: fromOptU32Amino(value.issuer_grantor_validation_validity_period),
verifierGrantorValidationValidityPeriod: fromOptU32Amino(value.verifier_grantor_validation_validity_period),
issuerValidationValidityPeriod: fromOptU32Amino(value.issuer_validation_validity_period),
verifierValidationValidityPeriod: fromOptU32Amino(value.verifier_validation_validity_period),
holderValidationValidityPeriod: fromOptU32Amino(value.holder_validation_validity_period),
issuerPermManagementMode: (value.issuer_perm_management_mode) ,
verifierPermManagementMode: (value.verifier_perm_management_mode) ,
}),
};

/**
* Amino converter for MsgUpdateCredentialSchema
*/
export const MsgUpdateCredentialSchemaAminoConverter = {
aminoType: '/verana.cs.v1.MsgUpdateCredentialSchema',
toAmino: (msg: MsgUpdateCredentialSchema) => clean({
creator: msg.creator ?? '',
id: u64ToStr(msg.id),
issuer_grantor_validation_validity_period: toOptU32Amino(msg.issuerGrantorValidationValidityPeriod),
verifier_grantor_validation_validity_period: toOptU32Amino(msg.verifierGrantorValidationValidityPeriod),
issuer_validation_validity_period: toOptU32Amino(msg.issuerValidationValidityPeriod),
verifier_validation_validity_period: toOptU32Amino(msg.verifierValidationValidityPeriod),
holder_validation_validity_period: toOptU32Amino(msg.holderValidationValidityPeriod),
}),
fromAmino: (value: any) => MsgUpdateCredentialSchema.fromPartial({
creator: value.creator ?? '',
id: strToU64(value.id),
issuerGrantorValidationValidityPeriod: fromOptU32Amino(value.issuer_grantor_validation_validity_period),
verifierGrantorValidationValidityPeriod: fromOptU32Amino(value.verifier_grantor_validation_validity_period),
issuerValidationValidityPeriod: fromOptU32Amino(value.issuer_validation_validity_period),
verifierValidationValidityPeriod: fromOptU32Amino(value.verifier_validation_validity_period),
holderValidationValidityPeriod: fromOptU32Amino(value.holder_validation_validity_period),
}),
};

/**
* Amino converter for MsgArchiveCredentialSchema
*/
export const MsgArchiveCredentialSchemaAminoConverter = {
aminoType: '/verana.cs.v1.MsgArchiveCredentialSchema',
// Proto → Amino JSON
toAmino: (msg: MsgArchiveCredentialSchema) => clean({
creator: msg.creator ?? '',
id: u64ToStr(msg.id),
archive: msg.archive ? true : undefined, // omit if false
}),
// Amino JSON → Proto
fromAmino: (value: any): MsgArchiveCredentialSchema =>
MsgArchiveCredentialSchema.fromPartial({
creator: value.creator,
id: strToU64(value.id),
archive: value.archive ?? false,
}),
};
72 changes: 72 additions & 0 deletions ts-proto/src/amino-converter/dd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client';

import { AminoConverter } from '@cosmjs/stargate'
import { MsgAddDID, MsgRenewDID, MsgTouchDID, MsgRemoveDID } from '../codec/verana/dd/v1/tx'

/**
* Amino converter for MsgAddDID
*/
export const MsgAddDIDAminoConverter: AminoConverter = {
aminoType: '/verana.dd.v1.MsgAddDID',
toAmino: (msg: MsgAddDID) => ({
creator: msg.creator,
did: msg.did,
years: msg.years,
}),
fromAmino: (value: any) =>
MsgAddDID.fromPartial({
creator: value.creator,
did: value.did,
years: value.years,
}),
}

/**
* Amino converter for MsgRenewDID
*/
export const MsgRenewDIDAminoConverter: AminoConverter = {
aminoType: '/verana.dd.v1.MsgRenewDID',
toAmino: (msg: MsgRenewDID) => ({
creator: msg.creator,
did: msg.did,
years: msg.years,
}),
fromAmino: (value: any) =>
MsgRenewDID.fromPartial({
creator: value.creator,
did: value.did,
years: value.years,
}),
}

/**
* Amino converter for MsgTouchDID
*/
export const MsgTouchDIDAminoConverter: AminoConverter = {
aminoType: '/verana.dd.v1.MsgTouchDID',
toAmino: (msg: MsgTouchDID) => ({
creator: msg.creator,
did: msg.did,
}),
fromAmino: (value: any) =>
MsgTouchDID.fromPartial({
creator: value.creator,
did: value.did,
}),
}

/**
* Amino converter for MsgRemoveDID
*/
export const MsgRemoveDIDAminoConverter: AminoConverter = {
aminoType: '/verana.dd.v1.MsgRemoveDID',
toAmino: (msg: MsgRemoveDID) => ({
creator: msg.creator,
did: msg.did,
}),
fromAmino: (value: any) =>
MsgRemoveDID.fromPartial({
creator: value.creator,
did: value.did,
}),
}
Loading
Loading