一键导入
align-proto
// Detect proto package changes (indexer-proto-ts-v2, abacus-proto-ts-v2, tc-abacus-proto-ts-v2), compare old vs new proto files, and implement new/modified gRPC services in sdk-ts following established patterns.
// Detect proto package changes (indexer-proto-ts-v2, abacus-proto-ts-v2, tc-abacus-proto-ts-v2), compare old vs new proto files, and implement new/modified gRPC services in sdk-ts following established patterns.
| name | align-proto |
| description | Detect proto package changes (indexer-proto-ts-v2, abacus-proto-ts-v2, tc-abacus-proto-ts-v2), compare old vs new proto files, and implement new/modified gRPC services in sdk-ts following established patterns. |
| user-invocable | true |
| allowed-tools | Bash, Read, Write, Edit, Glob, Grep, Agent, TodoWrite, AskUserQuestion |
You are an AI that detects proto package changes and implements new gRPC services in the SDK. Follow this workflow exactly.
$ARGUMENTS may contain --check-only to run in read-only mode (detect and report only, no implementation).| Package | Import Prefix | Base Class | Module Enum Source |
|---|---|---|---|
@injectivelabs/indexer-proto-ts-v2 | indexer-proto-ts-v2 | BaseIndexerGrpcConsumer | IndexerModule from ../types/index.js |
@injectivelabs/abacus-proto-ts-v2 | abacus-proto-ts-v2 | BaseAbacusGrpcConsumer | AbacusModule from ../types/index.js |
@injectivelabs/tc-abacus-proto-ts-v2 | tc-abacus-proto-ts-v2 | BaseGrpcConsumer | IndexerErrorModule from @injectivelabs/exceptions |
Ask the user which proto package to sync:
indexer-proto-ts-v2 (Indexer gRPC services)abacus-proto-ts-v2 (Abacus gRPC services)tc-abacus-proto-ts-v2 (TC Abacus gRPC services)packages/sdk-ts/package.json1.18.7) or says "latest"npm view @injectivelabs/{package-name} versionCreate a todo list to track progress:
Update each item as it completes.
Create backup directory: scripts/.align-proto-backup/{package-name}/{current-version}/
Copy all proto files from the package's node_modules generated directory:
node_modules/.pnpm/@injectivelabs+indexer-proto-ts-v2@{version}/node_modules/@injectivelabs/indexer-proto-ts-v2/generated/node_modules/.pnpm/@injectivelabs+abacus-proto-ts-v2@{version}/node_modules/@injectivelabs/abacus-proto-ts-v2/generated/node_modules/.pnpm/@injectivelabs+tc-abacus-proto-ts-v2@{version}/node_modules/@injectivelabs/tc-abacus-proto-ts-v2/generated/Update packages/sdk-ts/package.json with the new version. This change is permanent.
Run pnpm install --force in project root. The --force flag ensures all packages are properly re-linked from the pnpm store, preventing empty package directories from stale/corrupted installs. If install fails, show error and exit.
Compare backup vs new proto files. Detect:
*_rpc_pb.d.ts files)Use diff -rq for a file-level summary, then diff individual files for details. For modified services, use awk '/^export interface MessageName/,/^}/' on both versions to isolate and compare individual message definitions cleanly (raw .d.ts diffs are hard to read because messages are reordered between generator runs).
When an existing *_rpc_pb.d.ts is modified, do NOT assume the changes are purely additive. Check for each of these change types:
protobuf rpc: entries in *_rpc_pb.client.d.tsexport interface declarationsmessageType string fields — check doc comments like Type: 'a', 'b', 'c' on bidirectional/WS stream wrapper responses (e.g. TakerStreamResponse.messageType). Added values require new case arms in the corresponding WS stream handler, even when no new gRPC method was added.conditionalOrder?: ... sibling to existing quote?, requestAck?. These deliver payload for the new messageType value above.For each detected change, map it to SDK-side updates:
fetch* / stream* method; wire onto the request builderfetch* / stream* method, transformer method, type interface(s), and spec testsmessageType value → add case arm in WS stream handler + new event key in the *StreamEvents interfacePresent detected changes. Ask which services to implement. Skip old missing services.
If --check-only: Stop here. Show implementation status report and exit.
For each selected service:
Parse the proto *_rpc_pb.d.ts file to extract:
ServiceType definitionserverStreaming: true)Categorize methods:
fetch* methodstream* methodTakerStream, MakerStream) → extend existing IndexerWs*Stream.ts handlers; do NOT generate a new *StreamV2.ts classFind the most similar existing implementation as reference:
IndexerGrpcMetaApiIndexerGrpcRfqApiIndexerGrpcSpotStreamV2IndexerGrpcDerivativesApiIndexerWsTakerStream / IndexerWsMakerStreamFor modified services, the proposal is a surgical edit list — NOT a file-creation list. Enumerate each existing file that needs editing with the specific add/remove/rename per field. Call out breaking removals explicitly (they affect downstream consumers).
Show file list (or edit list), method breakdown, line estimates. Ask for approval.
Create files in dependency order:
1. types/*.ts (no dependencies)
2. transformers/*Transformer.ts (depends on types)
3. transformers/*StreamTransformer.ts (depends on main transformer)
4. grpc/*Api.ts (depends on types + transformers)
5. grpc/*Api.spec.ts (depends on API class + transformers)
6. grpc_stream/streamV2/*StreamV2.ts (depends on stream transformer)
7. grpc_stream/streamV2/*StreamV2.spec.ts (depends on stream class)
8. Update all index.ts exports (depends on all above)
types/*.ts - TypeScript interfacestransformers/IndexerGrpc*Transformer.ts (or AbacusGrpc* / TcAbacusGrpc*) - Data transformationgrpc/IndexerGrpc*Api.ts (or AbacusGrpc* / TcAbacusGrpc*) - Main API classgrpc/IndexerGrpc*Api.spec.ts (or AbacusGrpc* / TcAbacusGrpc*) - Test file for the API classtransformers/Indexer*StreamTransformer.ts - Stream transformationgrpc_stream/streamV2/IndexerGrpc*StreamV2.ts - Stream API classgrpc_stream/streamV2/IndexerGrpc*StreamV2.spec.ts - Stream test filegrpc/index.tstransformers/index.tstypes/index.tsgrpc_stream/index.ts (if streaming)All exports must be in strict alphabetical order.
@injectivelabs/exceptions was modified (new module added), build it first:
cd packages/exceptions && pnpm build
pnpm type-check in packages/sdk-tspnpm build in packages/sdk-ts (or pnpm tsdown if type-check has pre-existing errors)Ask: "Delete backup files?" If approved, delete scripts/.align-proto-backup/{package-name}/{version}/.
When debugging broken EIP712 generation for a Msg* abstraction:
*.spec.ts by changing the test message params and prepareEip712 args to match the client payload exactly: account number, chain ID, fee, gas, memo, sequence, timeout height, signer addresses, and every message field. Preserve omitted optional fields as omitted in the SDK params if the client omitted them.const txResponse = await new IndexerGrpcWeb3GwApi(
endpoints.indexer,
).prepareEip712Request({
txResponse. For EIP712 v2, parse message.context and message.msgs before diffing when useful because they are stringified JSON."0" or decimal strings. The SDK must sign the normalized value if Web3Gw includes it.Msg* abstraction so toEip712V2()/toWeb3Gw() matches the chain/Web3Gw output. Keep the regression test parameters aligned with the broken client payload and add a focused assertion for any optional-field defaulting behavior.// For Indexer:
import type * as ProtoPackagePb from '@injectivelabs/indexer-proto-ts-v2/generated/[proto_file]_pb'
// For Abacus:
import type * as ProtoPackagePb from '@injectivelabs/abacus-proto-ts-v2/generated/[proto_file]_pb'
// For TC Abacus:
import type * as TcAbacusPb from '@injectivelabs/tc-abacus-proto-ts-v2/generated/[proto_file]_pb'
export interface DataType {
field1: string
field2: number // Convert bigint to number for timestamps
}
export interface ListResponse {
items: DataType[]
next: string[] // Pagination tokens (indexer only)
}
export type GrpcDataType = ProtoPackagePb.DataType
Type naming conflicts: If the service introduces types that conflict with existing ones (e.g., PositionDelta already in derivatives), use a service-specific prefix (e.g., TCPositionDelta). Always check for conflicts before implementing.
import type * as ProtoPackagePb from '@injectivelabs/indexer-proto-ts-v2/generated/[proto_file]_pb'
import type { DataType, GrpcDataType, ListResponse } from '../types'
/**
* @category Indexer Grpc Transformer
*/
export class IndexerGrpc[Service]Transformer {
static grpcDataTypeToDataType(grpcData: GrpcDataType): DataType {
return {
field1: grpcData.field1,
field2: Number(grpcData.field2), // Convert bigint
}
}
static listResponseToItems(response: ProtoPackagePb.ListResponse): ListResponse {
return {
items: response.items.map(IndexerGrpc[Service]Transformer.grpcDataTypeToDataType),
next: response.next,
}
}
}
import * as ProtoPackagePb from '@injectivelabs/indexer-proto-ts-v2/generated/[proto_file]_pb'
import { [ServiceClient] } from '@injectivelabs/indexer-proto-ts-v2/generated/[proto_file]_pb.client'
import { IndexerModule } from '../types/index.js'
import { IndexerGrpc[Service]Transformer } from '../transformers/index.js'
import BaseIndexerGrpcConsumer from '../../base/BaseIndexerGrpcConsumer.js'
/**
* @category Indexer Grpc API
*/
export class IndexerGrpc[Service]Api extends BaseIndexerGrpcConsumer {
protected module: string = IndexerModule.[ServiceName]
private get client() {
return this.initClient([ServiceClient])
}
async fetchItems(params?: {
marketIds?: string[]
perPage?: number
token?: string
}) {
const { marketIds, perPage, token } = params || {}
const request = ProtoPackagePb.ItemsRequest.create()
if (marketIds && marketIds.length > 0) {
request.marketIds = marketIds
}
if (token) {
request.token = token
}
if (perPage) {
request.perPage = perPage
}
const response = await this.executeGrpcCall<
ProtoPackagePb.ItemsRequest,
ProtoPackagePb.ItemsResponse
>(request, this.client.items.bind(this.client))
return IndexerGrpc[Service]Transformer.listResponseToItems(response)
}
}
import { IndexerGrpc[Service]Transformer } from './IndexerGrpc[Service]Transformer.js'
import type * as ProtoPackagePb from '@injectivelabs/indexer-proto-ts-v2/generated/[proto_file]_pb'
/**
* @category Indexer Stream Transformer
*/
export class Indexer[Service]StreamTransformer {
static itemStreamCallback = (response: ProtoPackagePb.StreamItemResponse) => {
const item = response.item
return {
item: item ? IndexerGrpc[Service]Transformer.grpcItemToItem(item) : undefined,
operationType: response.operationType,
timestamp: Number(response.timestamp),
}
}
}
import * as ProtoPackagePb from '@injectivelabs/indexer-proto-ts-v2/generated/[proto_file]_pb'
import { [ServiceClient] } from '@injectivelabs/indexer-proto-ts-v2/generated/[proto_file]_pb.client'
import { createStreamSubscriptionV2 } from './streamHelpersV2.js'
import { Indexer[Service]StreamTransformer } from '../../transformers/index.js'
import { GrpcWebRpcTransport } from '../../../base/GrpcWebRpcTransport.js'
import type { StreamSubscription } from '../../../../types/index.js'
export type ItemStreamCallbackV2 = (
response: ReturnType<typeof Indexer[Service]StreamTransformer.itemStreamCallback>,
) => void
export class IndexerGrpc[Service]StreamV2 {
private client: [ServiceClient]
private transport: GrpcWebRpcTransport
constructor(endpoint: string, metadata?: Record<string, string>) {
this.transport = new GrpcWebRpcTransport(endpoint, metadata)
this.client = new [ServiceClient](this.transport)
}
streamItems({
marketIds,
callback,
}: {
marketIds?: string[]
callback: ItemStreamCallbackV2
}): StreamSubscription {
if (typeof callback !== 'function') {
throw new Error('callback must be a function')
}
const request = ProtoPackagePb.StreamItemRequest.create()
if (marketIds && marketIds.length > 0) {
request.marketIds = marketIds
}
const stream = this.client.streamItem(request)
return createStreamSubscriptionV2(stream, (response) => {
const transformed = Indexer[Service]StreamTransformer.itemStreamCallback(response)
callback(transformed)
})
}
}
import { Network, getNetworkEndpoints } from '@injectivelabs/networks'
import { IndexerGrpc[Service]Api } from './IndexerGrpc[Service]Api.js'
import type { IndexerGrpc[Service]Transformer } from '../transformers/index.js'
const endpoints = getNetworkEndpoints(Network.Mainnet)
const indexerGrpc[service]Api = new IndexerGrpc[Service]Api(endpoints.indexer)
describe('IndexerGrpc[Service]Api', () => {
test('fetchItems', async () => {
try {
const response = await indexerGrpc[service]Api.fetchItems({
// provide realistic test parameters
})
expect(response).toBeDefined()
expect(response).toEqual(
expect.objectContaining<
ReturnType<
typeof IndexerGrpc[Service]Transformer.listResponseToItems
>
>(response),
)
} catch (e) {
console.error(
'IndexerGrpc[Service]Api.fetchItems => ' + (e as any).message,
)
}
})
})
Test file conventions:
Network.Mainnet endpointsexpect.objectContaining with transformer return types for type validationtype only (used for type checking, not runtime)import { Network, getNetworkEndpoints } from '@injectivelabs/networks'
import { IndexerGrpc[Service]StreamV2 } from './IndexerGrpc[Service]StreamV2.js'
const endpoints = getNetworkEndpoints(Network.MainnetSentry)
describe('IndexerGrpc[Service]StreamV2', () => {
let stream: IndexerGrpc[Service]StreamV2
beforeEach(() => {
stream = new IndexerGrpc[Service]StreamV2(endpoints.indexer)
})
describe('constructor', () => {
it('should create instance with endpoint', () => {
expect(stream).toBeDefined()
expect(stream).toBeInstanceOf(IndexerGrpc[Service]StreamV2)
})
it('should create instance with endpoint and metadata', () => {
const metadata = { 'x-custom-header': 'test-value' }
const s = new IndexerGrpc[Service]StreamV2(endpoints.indexer, metadata)
expect(s).toBeDefined()
expect(s).toBeInstanceOf(IndexerGrpc[Service]StreamV2)
})
})
describe('streamItems', () => {
it('should create subscription with unsubscribe method', () => {
const subscription = stream.streamItems({
callback: () => {},
})
expect(subscription).toBeDefined()
expect(typeof subscription.unsubscribe).toBe('function')
subscription.unsubscribe()
})
it('should throw error if callback is not a function', () => {
expect(() => {
stream.streamItems({
callback: null as any,
})
}).toThrow('callback must be a function')
})
})
})
Stream test file conventions:
Network.MainnetSentry endpoints (not Network.Mainnet)subscription.unsubscribe() after creation testsThe module field references module enums from @injectivelabs/exceptions in packages/exceptions/src/exceptions/types/modules.ts:
IndexerModule.ServiceName → 'indexer-{service-name}'AbacusModule.ServiceName → 'abacus-{service-name}'IndexerErrorModule.Abacus → 'abacus'When implementing a new service, add the module to the exceptions package and build it before building sdk-ts.
.js extensionparams || {}length > 0 before assignmentthis.client.method.bind(this.client)typeof callback !== 'function'/** @category */The codebase may have pre-existing TypeScript errors. To verify your implementation:
pnpm tsdown directly if type-check shows many errors - the build may succeed.Cannot find module '@protobuf-ts/runtime-rpc'Property 'toBinary' does not exist on typeArgument of type 'unknown' is not assignable