Documentation Index
Fetch the complete documentation index at: https://docs.kiruse.dev/llms.txt
Use this file to discover all available pages before exploring further.
Apophis allows integrating new types for de/serialization with standard encodings (currently
ProtoBuf and Amino) by following a specific pattern.
Generally, the default encodings expect your messages to be implemented as classes. The ProtoBuf and
Amino encoding subsystems define the following types, as well as these methods for registration of
new default types:
@apophis-sdk/core/encoding/protobuf/any.ts
export type ProtobufType<T1 extends string = string, T2 = any> = {
get protobufTypeUrl(): T1;
toProtobuf(value: T2): Uint8Array;
fromProtobuf(value: Uint8Array): T2;
};
export declare function registerDefaultProtobuf(...types: ProtobufType[]): void;
@apophis-sdk/cosmos/encoding/amino.ts
export type AminoType<T1 extends string = string, T2 = any> = {
get aminoTypeUrl(): T1;
new(data: T2): { get data(); T2 }
};
export declare function registerDefaultAmino(...types: AminoType[]): void;
With v0.3.1, a new solution for protobufs is introduced and will eventually supersede the existing
in v0.4.0:
import { IMessage } from '@kiruse/hiproto';
export type ProtobufSchemaType<T1 extends string = string, T2 = any> = {
get protobufTypeUrl(): T1;
get protobufSchema(): IMessage<any, T2>;
new (data: T2): any;
};
This new method leverages a new in-house protobuf abstraction hiproto
which uses a zod-like API to define protobuf schemas.
Correspondingly, the Cosmos Bank Send message is implemented as follows:
import { registerDefaultProtobufs } from '@apophis-sdk/core/encoding/protobuf/any.js';
import type { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin';
import { MsgSend } from 'cosmjs-types/cosmos/bank/v1beta1/tx';
import { registerDefaultAminos } from '../encoding/amino';
export namespace Bank {
export type SendData = {
fromAddress: string;
toAddress: string;
amount: Coin[];
}
export class Send {
static readonly protobufTypeUrl = '/cosmos.bank.v1beta1.MsgSend';
static readonly aminoTypeUrl = 'cosmos-sdk/MsgSend';
constructor(public data: SendData) {}
static toProtobuf(value: Send): Uint8Array {
return MsgSend.encode(MsgSend.fromPartial(value.data)).finish();
}
static fromProtobuf(value: Uint8Array): Send {
const { fromAddress, toAddress, amount } = MsgSend.decode(value);
return new Send({ fromAddress, toAddress, amount });
}
};
registerDefaultProtobufs(Send);
registerDefaultAminos(Send);
}
As the naming suggests, these are merely “default” types. Using the
Middleware Subsystem, nearly every single
aspect of the Apophis SDK can be fine-tuned to the requirements of a specific blockchain, with these
default types used as fallbacks.
The beauty of this approach is that the same message class can be used for a variety of blockchains,
and can even be somewhat standardized across entirely different blockchains. Likewise, it allows
deserializing transactions into semantic objects, rather than just raw bytes or plain data objects.
Amino
Amino is a legacy JSON sub-specification for blockchains that use the Cosmos SDK. It is superceded
by ProtoBuf, but is still widely used, and the only format Ledger hardware wallets
support in Cosmos.
Amino is defined in the @apophis-sdk/cosmos package, but listed here for completeness.
ProtoBuf
ProtoBuf is a widely adopted data transmission format developed and
primarily maintained by Google. Unlike Amino, it is a size-optimized binary format, and requires
more complex de/serialization.
Formerly, you had to provide the correct de/serialization for your protobuf types yourself. With
v0.3.1, we are now migrating to hiproto. Following is an
example of how I’ve implemented the Coin schema:
import hpb from '@kiruse/hiproto';
const pbCoin = hpb.message({
denom: hpb.string(1),
amount: hpb.string(2).transform<bigint>({
encode: (value) => value.toString(),
decode: (value) => BigInt(value),
default: 0n,
}),
});
You can then reference this schema in your own messages:
import { pbCoin } from '@apophis-sdk/cosmos/encoding/protobuf/core';
const pbMsgSend = hpb.message({
fromAddress: hpb.string(1),
toAddress: hpb.string(2),
amount: hpb.repeated.array(3, pbCoin),
});
And de/serialize it as follows:
import { Cosmos } from '@apophis-sdk/cosmos';
import { pbMsgSend } from './protobuf/schema.js';
const encoded = pbMsgSend.encode({
fromAddress: 'cosmos1...',
toAddress: 'cosmos1...',
amount: [Cosmos.coin(1_000_000n, 'untrn')],
}).toShrunk().toUint8Array();
const decoded = pbMsgSend.decode(encoded);
console.log(`${decoded.fromAddress} -> ${decoded.toAddress}: ${decoded.amount.length} coins`);
You can then register your message class with the registerDefaultProtobufSchema function:
import { registerDefaultAminos, registerDefaultProtobufSchema } from '@apophis-sdk/core/encoding/protobuf/any.js';
import hpb from '@kiruse/hiproto';
import { pbMsgSend } from './protobuf/schema.js';
export namespace Bank {
export type SendData = hpb.infer<typeof pbMsgSend>;
export class Send {
static readonly aminoTypeUrl = 'cosmos-sdk/MsgSend';
static readonly protobufTypeUrl = '/cosmos.bank.v1beta1.MsgSend';
static readonly protobufSchema = pbMsgSend;
constructor(public data: SendData) {}
}
registerDefaultAminos(Send);
registerDefaultProtobufSchema(Send);
}
Note that this is currently in a transitional state. For now, Apophis’ Cosmos module still uses the
cosmjs-types package for its standard messages, but these
will be gradually migrated to the new hiproto-based approach. The existing methodology is now
marked as deprecated and will be removed in v0.4.0.
Custom Amino Marshaller
You can further apply a custom marshaller for Amino types. This is useful where data is no longer
reliably reconstructible from serialized form. For example the StoreCode message of the CosmWasm
module:
import { fromBase64 } from '@apophis-sdk/core/utils.js';
import { registerDefaultAminos } from '@apophis-sdk/cosmos/encoding/amino.js';
import hpb from '@kiruse/hiproto';
import { extendDefaultMarshaller, RecaseMarshalUnit } from '@kiruse/marshal';
const marshaller = extendDefaultMarshaller([
RecaseMarshalUnit(toSnakeCase, toCamelCase),
]);
const pbStoreCode = hpb.message({
sender: hpb.string(1).required(),
wasmByteCode: hpb.bytes(2).required(),
});
type StoreCodeData = hpb.infer<typeof pbStoreCode>;
class StoreCode {
static readonly aminoTypeUrl = 'wasm/MsgStoreCode';
static readonly aminoMarshaller = addMarshallerFinalizer<StoreCodeData>(marshaller, value => ({
...value,
wasmByteCode: fromBase64(value.wasmByteCode),
}));
static readonly protobufTypeUrl = '/cosmwasm.wasm.v1.MsgStoreCode';
static readonly protobufSchema = pbStoreCode;
constructor(public data: hpb.infer<typeof pbStoreCode>) {}
}
registerDefaultAminos(StoreCode);
registerDefaultProtobufSchema(StoreCode);
function toSnakeCase(str: string): string { /* ... */ }
function toCamelCase(str: string): string { /* ... */ }
Required Fields
By default, Protobuf message fields are all optional. Even so, clients may require certain fields
be present or otherwise transactions are rejected. Cross-compatibility with Amino also may require
certain fields to be present, even if empty, such as funds on a Contracts.Execute message.
Unfortunately, for legacy Amino support, you must pay attention to required and optional fields
yourself. Otherwise, the full node may reconstruct the signature payload differently from the data
you have provided to the signer, causing the transaction to be rejected immediately.
The Contracts.Execute schema may look like this:
import { pbCoin } from '@apophis-sdk/cosmos/encoding/protobuf/core.js';
import hpb from '@kiruse/hiproto';
const pbExecute = hpb.message({
sender: hpb.string(1).required(),
contract: hpb.string(2).required(),
msg: hpb.bytes(3).required(),
funds: hpb.repeated.array(5, pbCoin).required(),
});
Given this schema, you shouldn’t need any further changes to the default pattern.
There are a few general-purpose transforms for protobuf fields available in
@apophis-sdk/cosmos/encoding/protobuf/core.js:
- bigintTransform: Transforms a
string to a bigint and vice versa.
- aminoTransform: Applies
Amino.normalize to an arbitrary value, including sorting the object’s
keys. In general, whenever you use hpb.json, you should also apply this transform.
Other encodings
Other blockchains such as Solana and Ethereum use different encodings. When the time comes to
develop integrations for these ecosystems, I will add corresponding encoding subsystems to this SDK.
If you are looking to integrate your own encoding, please follow the above pattern, and develop a
corresponding middleware. Your middleware may use the ["core", "init"] hook to register existing
messages (if applicable, e.g. Bank.Send) for your new encoding so SDK consumers may seamlessly
integrate your blockchain.