Skip to main content
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.
I