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.
Of course, I use Apophis myself to build my own projects. So here are some examples of what I’ve built with Apophis.
DeTFL
TFL was the founding team of the Terra ecosystem. After the Great Death Spiral, in the wake of the SEC lawsuit, TFL was forced to build non-financial dapps. One of these was Enterprise, a competitor to DAODAO.
DeTFL is a simple UI for TFL’s Enterprise protocols, specifically to recover stuck assets in Enterprise DAOs.
Here are some snippets pertaining to the use of Apophis in the DeTFL project. Note that this project uses Preact and supports signals natively. For React, you’ll need to setup your build pipeline, or use the useSignals() hook.
import { CosmosNetworkConfig } from '@apophis-sdk/core';
import { CosmWasm } from '@apophis-sdk/cosmwasm';
import { toast } from '@kiruse/cosmos-components';
// Make a few smart queries to check the version of the DAO on-chain
async function checkVersion(network: CosmosNetworkConfig, govCtrlContract: string) {
const { enterprise_contract: enterpriseContract } = await CosmWasm.query.smart<any>(
network,
govCtrlContract,
{ config: {} },
);
const { dao_version } = await CosmWasm.query.smart<any>(
network,
enterpriseContract,
{ dao_info: {} },
);
const ver = `${dao_version.major}.${dao_version.minor}.${dao_version.patch}`;
console.info(`DAO ${govCtrlContract} is on version ${ver}`);
if (ver !== '1.2.1') toast.warn('This DAO is not on version 1.2.1. This tool might not work as expected.');
}
import { signals as apophisSignals, type CosmosNetworkConfig } from '@apophis-sdk/core';
import { CosmWasm } from '@apophis-sdk/cosmwasm';
import { toast } from '@kiruse/cosmos-components';
import { Decimal } from '@kiruse/decimal';
import { useComputed } from 'preact/signals';
import { useAsyncComputed } from '~/hooks/useAsyncComputed.js';
import { impersonateAddress, refreshCounter } from '~/state.js';
function TokenRecovery({ address }: { address: string }) {
const userAddress = useComputed(() => impersonateAddress.value || apophisSignals.address.value);
const stake = useAsyncComputed(async () => {
// refreshCounter is used to trigger re-calculation after a tx
// b/c blockchain queries aren't reactive
console.log('Refresh counter:', refreshCounter.value);
try {
const network = apophisSignals.network.value as CosmosNetworkConfig;
if (!network) throw new Error('No network selected');
if (!address) throw new Error('Failed to get Membership Contract address');
if (!userAddress.value) throw new Error('Please provide an address');
const { weight } = await CosmWasm.query.smart<any>(
network,
address,
{ user_weight: { user: userAddress.value } },
);
const { claims: pending } = await CosmWasm.query.smart<any>(
network,
address,
{ claims: { user: userAddress.value } },
);
const { claims: claimable } = await CosmWasm.query.smart<any>(
network,
address,
{ releasable_claims: { user: userAddress.value } },
);
return {
total: BigInt(weight),
pending,
claimable,
}
} catch (err) {
toast.errorlink(err);
throw err;
}
});
// The decimals here (6) could be pulled from the network config, but I was too lazy
const stakedTokens = useComputed(() => new Decimal(stake.value?.total ?? 0, 6));
// Claims are objects with an amount and a timestamp, so we just add them up here
const pendingTokens = useComputed(() =>
stake.value.pending.reduce((acc, claim) => acc.add(new Decimal(BigInt(claim.amount), 6)), new Decimal(0, 6))
);
const claimableTokens = useComputed(() =>
stake.value.claimable.reduce((acc, claim) => acc.add(new Decimal(BigInt(claim.amount), 6)), new Decimal(0, 6))
);
// ... more code
return (
<>
{/* ... more code ... */}
{!!stake.value && (
<>
<p class="mb-2">
{/* Again, the denom could be pulled from the network config, but I was too lazy */}
You have <cosmos-balance value={stakedTokens} denom="tokens" /> tokens staked,{' '}
<cosmos-balance value={pendingTokens} denom="tokens" /> pending, and{' '}
<cosmos-balance value={claimableTokens} denom="tokens" /> claimable.
</p>
</>
)}
</>
);
}
Check out the full source code here. It includes an example for Transactions too! But the whole file is rather lengthy.
Centauri
Centauri was supposed to become a GUI for the TokenFactory, until I realized that the REST API doesn’t fully support the TokenFactory and returns “not implemented” errors. I’m not sure if it would work with gRPC, but Apophis does not currently support gRPC.
Nonetheless, Centauri serves as a good example for integrating new Chain Modules. The TokenFactory is a unique example of a widely used Chain Module, so without Apophis, you’d need to install one Chain-specific library for each Chain you wish to support. Apophis can support any chain that adheres to the original API.
Here’s how I integrated the TokenFactory with Apophis:
import type { CosmosNetworkConfig } from '@apophis-sdk/core';
import { registerDefaultProtobufSchema } from '@apophis-sdk/core/encoding/protobuf/any.js';
import { Cosmos, registerDefaultAmino } from '@apophis-sdk/cosmos';
import { pbCoin } from '@apophis-sdk/cosmos/encoding/protobuf/core.js';
import hpb from '@kiruse/hiproto';
import { RestMethods } from '@kiruse/restful';
export namespace TokenFactory {
// Metadata is now in the Apophis Cosmos module
// import { Bank } from '@apophis-sdk/cosmos/msg/bank.js';
// type TokenMetadata = Bank.DenomMetadata;
export type TokenMetadata = hpb.infer<typeof pbMetadata>;
export type DenomUnit = Required<hpb.infer<typeof pbMetadata>>['denomUnits'][number];
// alternatively, use Bank.Query.pbMetadata
export const pbMetadata = hpb.message({
description: hpb.string(1),
denomUnits: hpb.repeated.submessage(2, {
denom: hpb.string(1),
exponent: hpb.uint32(2),
aliases: hpb.repeated.string(3),
}),
base: hpb.string(3),
display: hpb.string(4),
name: hpb.string(5),
symbol: hpb.string(6),
uri: hpb.string(7),
uriHash: hpb.string(8),
});
//#region Messages
//#region CreateDenom
export const pbMsgCreateDenomRequest = hpb.message({
sender: hpb.string(1),
subdenom: hpb.string(2),
});
export type CreateDenomData = hpb.infer<typeof pbMsgCreateDenomRequest>;
export class CreateDenom {
static readonly aminoTypeUrl = 'osmosis/tokenfactory/create-denom';
static readonly protobufTypeUrl = '/osmosis.tokenfactory.v1beta1.MsgCreateDenom';
static readonly protobufSchema = pbMsgCreateDenomRequest;
constructor(public data: CreateDenomData) {}
}
registerDefaultProtobufSchema(CreateDenom);
registerDefaultAminos(CreateDenom);
//#endregion CreateDenom
// ... more messages
//#endregion Messages
export namespace Query {
// Pseudo-type used to access the TokenFactory REST API
export type TokenFactoryRestApi = {
osmosis: {
tokenfactory: {
v1beta1: {
params: RestMethods<{
get(): hpb.infer<typeof Query.pbParamsRes>;
}>
denoms_from_creator: {
[creator: string]: RestMethods<{
get(): hpb.infer<typeof Query.pbDenomsFromCreatorRes>;
}>
}
} & {
[denom: string]: {
authority_metadata: RestMethods<{
get(): hpb.infer<typeof Query.pbDenomAuthorityMetadataRes>;
}>;
};
};
};
};
};
// internal helper to convert the REST API to the correct type
const getApi = (network: CosmosNetworkConfig) => Cosmos.rest(network) as unknown as TokenFactoryRestApi;
export async function params(network: CosmosNetworkConfig) {
const api = getApi(network);
// The REST API is built to reflect the actual URL you'd call, with every endpoint being a function
const res = await api.osmosis.tokenfactory.v1beta1.params('GET');
return res.params as typeof res.params;
}
export async function denomsFromCreator(network: CosmosNetworkConfig, creator: string) {
const api = getApi(network);
// Parameters in a URL are as part of the path, so we can just use a string index
const res = await api.osmosis.tokenfactory.v1beta1.denoms_from_creator[creator]('GET');
return res.denoms as typeof res.denoms;
}
// ... more queries
}
}
The CreateDenom region is all it takes to define a new message. No code generation required. Just find the type you need in the blockchain code and translate it.
While it’s still an advanced use case, it made my life as an independent Interchain developer vastly easier. Protobuf build pipelines in the Cosmos are a royal PITA, so I’m glad I don’t have to deal with them.
Find the full file & repo here.
Snap
Snap is currently a minimalist collection of scripts I use to take snapshots of NFT holders & stakers on DAODAO. It is extremely rudimentary & written in CoffeeScript. Eventually, I plan to build it into a full-fledged GUI.
The full script to collect NFT holders is fairly short. While I love CoffeeScript, I’d never use it for a serious project. It’s great for one-off scripts like this, but in larger projects with multiple contributors, its legibility is abysmal.
import { CosmWasm } from '@apophis-sdk/cosmwasm'
import fs from 'fs/promises'
import path from 'path'
import { networks } from '~/config.js'
# Require a path to a JSON file to store the state for recovery, in case of failure
if process.argv.length < 3
console.error "Usage: bun collect-nft-owners.coffee <path-to-store-json>"
process.exit 1
filepath = process.argv[2]
await fs.mkdir path.dirname(filepath), recursive: true
# Helper to save the state to the JSON file
save = -> await fs.writeFile filepath, JSON.stringify(store)
# Try to read the state from the JSON file, or initialize an empty object if the file doesn't exist
store = await fs.readFile(filepath, 'utf8')
.then (data) -> JSON.parse data
.catch -> {}
# Initialize the store if it doesn't exist
store ?= {}
store.tokenIds ?= []
store.owners ?= {}
store.ownersByToken ?= {}
# Network & contract address
# Note: Networks uses `Cosmos.getNetworkFromRegistry` and overrides the default endpoints
# The default endpoints from the Chain registry can be unreliable, so I recommend overriding them
network = await networks.terra2
contract = "terra17z7fpaa8kah698xn5tarrcucvualdy4wsztkfc404g3garucpu6qmxp50g"
# Collect a list of NFT Token IDs, or restore from store if available
if store.tokenIds.length is 0
console.log "Collecting token IDs..."
lastTokenId = undefined
first = true
# The query is paginated, so we need to loop until we get an empty response
while first or lastTokenId isnt undefined
first = false
response = await CosmWasm.query.smart network, contract,
all_tokens:
start_after: lastTokenId
limit: 100
lastTokenId = response.tokens[response.tokens.length - 1]
store.tokenIds.push response.tokens...
store.tokenIds = store.tokenIds.map String
await save()
else
console.log "Restored token IDs from #{filepath}"
# Unfortunately, there is no batch query for this, so we need to retrieve each owner one by one
console.log "Collecting owners..."
for tokenId in store.tokenIds
if store.ownersByToken[tokenId]
console.log "Skipping #{tokenId} because it already has an owner"
continue
console.log "Collecting owner for #{tokenId}..."
{ owner } = await CosmWasm.query.smart network, contract,
owner_of:
token_id: tokenId
store.owners[owner] ?= []
store.owners[owner].push tokenId
store.ownersByToken[tokenId] = owner
# Save after each iteration to avoid losing progress, which actually happens a lot
await save()
console.log "Done"
This is a nice example because it demonstrates how to use Apophis outside of a browser. Other libraries don’t support this, but it was actually important for my own needs, for example precisely because I needed to compile a whitelist for upcoming mints.