DubheSuiClient

Dubhe Client SDK

Dubhe provides a layered set of client packages for interacting with on-chain contracts, the indexer, and the ECS world from TypeScript/JavaScript.

Package Overview

PackagePurpose
@0xobelisk/client/suiRecommended entry point — bundles all clients in one call
@0xobelisk/reactReact Provider + hooks for frontend apps
@0xobelisk/sui-clientLow-level Sui contract client (Dubhe class)
@0xobelisk/graphql-clientApollo-based indexer GraphQL client
@0xobelisk/ecsEntity-Component-System query / subscription layer

@0xobelisk/client/sui — Recommended Entry Point

createClient initialises all clients at once and returns a DubheClientBundle. Use this for any project that is not using React (Node.js scripts, tests, non-React frontends).

Install

pnpm add @0xobelisk/client

Usage

import { createClient } from '@0xobelisk/client/sui';
import metadata from './metadata.json';
import { Network, PackageId, DappHubId, DappStorageId, FrameworkPackageId } from './deployment';
 
const client = createClient({
  network: Network, // 'mainnet' | 'testnet' | 'devnet' | 'localnet'
  packageId: PackageId,
  metadata, // SuiMoveNormalizedModules from loadMetadata()
  dappHubId: DappHubId, // DappHub shared object ID
  dappStorageId: DappStorageId, // DappStorage shared object ID
  frameworkPackageId: FrameworkPackageId, // only needed for localnet / devnet
  credentials: {
    secretKey: process.env.PRIVATE_KEY // base64 or hex private key
    // mnemonics: '...'                 // alternative: 12/24-word mnemonic
  },
  endpoints: {
    graphql: 'http://localhost:8080/graphql', // dubhe-indexer proxy (default port 8080)
    websocket: 'ws://localhost:8080/graphql',
    grpc: 'http://localhost:8085' // dubhe-indexer gRPC backend (default port 8085)
    // fullnodeUrls: ['https://...'],             // optional: override RPC endpoint
    // channelUrl: 'http://...',                  // optional: DubheChannel server
  }
  // options: {                                   // optional performance tuning
  //   enableBatchOptimization: true,
  //   cacheTimeout: 5000,
  //   debounceMs: 100,
  // }
});
 
// client.contract      — Dubhe instance (call Move functions)
// client.graphqlClient  — DubheGraphqlClient (query indexer via GraphQL)
// client.grpcClient     — DubheGrpcClient (query indexer via gRPC)
// client.ecsWorld       — DubheECSWorld (entity/component queries)
// client.address        — user wallet address string

Calling Move functions

import { Transaction } from '@mysten/sui/transactions';
 
// Send a transaction
const tx = new Transaction();
await client.contract.tx.player_system.level_up({ tx });
 
// Read-only query (devInspect)
const result = await client.contract.query.player_system.get_level({
  tx: new Transaction()
});

@0xobelisk/react — React Provider + Hooks

For React / Next.js apps. Wraps createClient in a Context Provider with useRef-based single-initialisation (no re-renders on config re-evaluation).

Install

pnpm add @0xobelisk/react

Setup: Wrap your app with DubheProvider

import { DubheProvider } from '@0xobelisk/react';
import metadata from './metadata.json';
import { Network, PackageId, DappHubId, DappStorageId, FrameworkPackageId } from './deployment';
 
function App() {
  return (
    <DubheProvider
      config={{
        network: Network,
        packageId: PackageId,
        metadata,
        dappHubId: DappHubId,
        dappStorageId: DappStorageId,
        frameworkPackageId: FrameworkPackageId,
        credentials: { secretKey: process.env.NEXT_PUBLIC_PRIVATE_KEY },
        endpoints: {
          graphql: 'http://localhost:8080/graphql',
          websocket: 'ws://localhost:8080/graphql',
          grpc: 'http://localhost:8085'
        }
      }}
    >
      <MyApp />
    </DubheProvider>
  );
}

Hooks

All hooks must be called inside a component tree wrapped by DubheProvider.

useDubhe — full bundle

import { useDubhe } from '@0xobelisk/react';
import { Transaction } from '@mysten/sui/transactions';
 
function PlayerCard() {
  const { contract, graphqlClient, grpcClient, ecsWorld, address } = useDubhe();
 
  const handleLevelUp = async () => {
    const tx = new Transaction();
    await contract.tx.player_system.level_up({ tx });
  };
 
  return <button onClick={handleLevelUp}>Level Up ({address})</button>;
}

useDubheContract — contract only

const contract = useDubheContract();
await contract.tx.my_system.my_action({ tx });

useDubheGraphQL — GraphQL client only

const graphqlClient = useDubheGraphQL();
const data = await graphqlClient.getAllTables('player', { first: 10 });

The GraphQL endpoint is the dubhe-indexer proxy at port 8080 (default: http://localhost:8080/graphql). Set it via endpoints.graphql in DubheProvider.

useDubheECS — ECS world only

const ecsWorld = useDubheECS();
const players = await ecsWorld.queryWith('player');

grpcClient from useDubhe

The full bundle returned by useDubhe() also contains grpcClient — a DubheGrpcClient instance connected to the dubhe-indexer gRPC backend (default port 8085):

const { grpcClient } = useDubhe();
 
const result = await grpcClient.dubheGrpcClient.queryTable({
  tableId: 'player',
  filters: [],
  pagination: { page: 0, pageSize: 20 }
});

useDubhe() also returns the config-derived network, packageId, dappKey, dappHubId, dappStorageId, and frameworkPackageId. The session hooks below read dappKey / dappHubId / frameworkPackageId from the provider, so set them in the DubheProvider config to enable session-key features.

useSessionKey — ephemeral session-key management

A session key is an ephemeral keypair that the user authorises on-chain via dapp_system::activate_session. While active, game actions are signed silently by the session wallet (no wallet popup), yet every on-chain identity still resolves to the main wallet (canonical_owner). State is scoped per (dappKey, network, owner) — switching accounts never leaks another account’s session. The keypair lives in IndexedDB; session metadata in localStorage.

import { useSessionKey } from '@0xobelisk/react';
 
function SessionControls({ owner, userStorageId }) {
  const {
    sessionAddress, // ephemeral wallet address
    isActive, // whether a valid local session exists
    minutesLeft, // remaining lifetime (UI hint)
    buildActivateTx, // (userStorageId, durationMs?) => Transaction
    confirmActivation, // call after the activate tx lands on-chain
    buildDeactivateTx, // (userStorageId) => Transaction
    signAndSend, // (buildFn) => sign + execute with the session key (no popup)
    clearSession, // wipe local state and rotate to a fresh keypair
    revalidate, // re-check the indexer's dubheSessions table
    getSessionBalance, // session wallet SUI balance
    buildFundSessionTx // (amountSui) => Transaction to top up the session wallet
  } = useSessionKey(owner, { durationMs: 60 * 60 * 1000 });
 
  const activate = async () => {
    const tx = buildActivateTx(userStorageId);
    await signWithMainWallet(tx); // your wallet adapter signs the activation
    confirmActivation();
  };
 
  return (
    <button onClick={activate}>Enable session ({isActive ? `${minutesLeft}m` : 'off'})</button>
  );
}

isActive is computed locally; revalidate() cross-checks the indexer’s dubheSessions row and drops stale local state when the session was revoked or replaced elsewhere (with a short grace window right after activation). The session wallet needs a small SUI balance to pay gas for silently-signed actions — use getSessionBalance / buildFundSessionTx to keep it topped up.

useDubheTx — transaction execution layer (session-first)

The recommended way to send game transactions. Wraps useSessionKey and adds automatic fee settlement plus signer selection. Wallet signing is injected via signWithWallet, so the hook has no dependency on any specific wallet library.

import { useDubheTx, SessionInvalidatedError } from '@0xobelisk/react';
 
function GameActions({ owner, userStorageId }) {
  const { contract } = useDubhe();
  const { execTx, execTxWithMainWallet, session } = useDubheTx({
    owner,
    userStorageId, // required so settle_writes can be prepended
    signWithWallet: async (tx) => walletAdapter.signAndExecute(tx), // must return { digest }
    settleThreshold: 1500n // optional, default 1500
  });
 
  // Game action: session-key silent-sign when active, else main wallet fallback.
  const move = async () => {
    try {
      const { digest, signer } = await execTx((tx) => {
        contract.tx.player_system.move_player({ tx });
      });
    } catch (e) {
      if (e instanceof SessionInvalidatedError) {
        // session revoked/expired on-chain — prompt the user to re-activate
      }
    }
  };
 
  // Actions that must come from the main wallet (onboarding, marketplace, admin):
  const buy = () => execTxWithMainWallet((tx) => contract.tx.market_system.buy({ tx }));
 
  return <button onClick={move}>Move</button>;
}

Behaviour:

  • execTx(buildFn) signs with the session key when session.isActive, otherwise with the main wallet, returning { digest, signer: 'session' | 'wallet' }. On a session-signing failure it revalidates and throws SessionInvalidatedError if the session is no longer valid on-chain.
  • When the user’s unsettled_count reaches settleThreshold, a settle_writes (or settle_writes_user_pays, depending on the DApp’s settlement_mode) call is automatically prepended to the PTB. Requires userStorageId and dappHubId.
  • execTxWithMainWallet(buildFn) always uses the injected wallet signer.

@0xobelisk/sui-client — Low-Level Contract Client

Use Dubhe directly when you need fine-grained control, or are not using the bundle factory.

Install

pnpm add @0xobelisk/sui-client

Usage

import { Dubhe, loadMetadata } from '@0xobelisk/sui-client';
import { Transaction } from '@mysten/sui/transactions';
import { Network, PackageId, DappStorageId, FrameworkPackageId } from './deployment';
 
const metadata = await loadMetadata(Network, PackageId);
 
const dubhe = new Dubhe({
  networkType: Network,
  packageId: PackageId,
  metadata,
  secretKey: process.env.PRIVATE_KEY,
  // mnemonics:       '...',          // alternative: 12/24-word mnemonic
  // fullnodeUrls:    ['https://...'], // override default RPC endpoint
  frameworkPackageId: FrameworkPackageId, // dubhe framework package ID (localnet/devnet only; testnet/mainnet use SDK defaults)
  dappStorageId: DappStorageId
  // channelUrl:      'http://...',    // DubheChannel endpoint (if using Channel)
});
 
const address = dubhe.getAddress();
 
// Send a transaction
const tx = new Transaction();
await dubhe.tx.player_system.level_up({
  tx,
  onSuccess: async (result) => {
    console.log('tx:', result.digest);
    await dubhe.waitForTransaction(result.digest);
  },
  onError: (err) => console.error(err)
});
 
// Read-only query
const result = await dubhe.query.player_system.get_level({ tx: new Transaction() });
const level = dubhe.view(result); // decode BCS return value

Transaction with raw PTB (wallet integration)

// Build the transaction without executing it
const rawTx = await dubhe.tx.player_system.level_up({
  tx,
  isRaw: true // returns the Transaction object instead of executing
});
 
// Sign and send via an external wallet adapter
const response = await dubhe.signAndSendTxn({
  tx: rawTx,
  onSuccess: (result) => console.log('success:', result.digest)
});

DubheParams Reference

type DubheParams = {
  // Authentication — all optional.
  // If neither secretKey nor mnemonics is provided, a random 24-word mnemonic is generated.
  secretKey?: string; // base64, hex, or bech32 private key
  mnemonics?: string; // 12 or 24 words, space-separated
 
  // Network
  networkType?: 'mainnet' | 'testnet' | 'devnet' | 'localnet'; // default: 'mainnet'
  fullnodeUrls?: string[]; // override default RPC endpoints
  faucetUrl?: string;
 
  // Contract
  packageId?: string; // your contract's package ID
  metadata?: SuiMoveNormalizedModules; // from loadMetadata()
 
  // Dubhe Framework (required for session / settlement features)
  frameworkPackageId?: string; // dubhe framework package ID — from deployment.ts FrameworkPackageId
  // needed for localnet and devnet; testnet/mainnet use SDK defaults
  dappStorageId?: string; // DappStorage object ID — from deployment.ts DappStorageId
 
  // DubheChannel (optional)
  channelUrl?: string; // channel server URL
};

Loading Metadata

metadata is required by all contract clients. Load it at startup:

import { loadMetadata } from '@0xobelisk/sui-client';
 
// Fetch from the network at runtime (one network call)
const metadata = await loadMetadata('testnet', '0x<packageId>');
 
// Or import a pre-generated JSON file (faster — no network call)
import metadata from './metadata.json';

To generate metadata.json locally after deployment:

dubhe load-metadata --network testnet --package-id 0x<packageId>

Deployment Artifacts

After dubhe publish, run dubhe store-config to generate a typed TypeScript file with all deployment IDs:

dubhe store-config --network testnet --output-ts-path ./src/deployment.ts
// src/deployment.ts — generated by dubhe store-config
export const Network: NetworkType = 'testnet';
export const PackageId = '0x...';
export const DappHubId = '0x...'; // DappHub shared object ID
export const DappStorageId = '0x...'; // DappStorage shared object ID
export const FrameworkPackageId = undefined; // set automatically for localnet

Import these values to initialise the client — do not import .history/…/latest.json directly.


UserStorage and Session Management

The Dubhe class exposes helper methods for the UserStorage lifecycle and session key management. All methods require frameworkPackageId and packageId to be set in the constructor.

initUserStorage

Create a UserStorage shared object for the signer. Each address may only call this once per DApp.

await dubhe.initUserStorage({
  dappHubId: DappHubId,
  dappStorageId: DappStorageId
});

getUserStorageId

Resolve the UserStorage object ID for a given address.

const userStorageId = await dubhe.getUserStorageId(userAddress);
if (!userStorageId) {
  // User has not initialised their storage yet
  await dubhe.initUserStorage({ dappHubId, dappStorageId });
}

getUserStorageFields

Read all fields from a UserStorage object.

const fields = await dubhe.getUserStorageFields(userStorageId);
// fields.canonical_owner, fields.write_count, fields.session_key, ...

getDappStorageFields

Read all metadata and fee-state fields from a DappStorage object.

const fields = await dubhe.getDappStorageFields(dappStorageId);
// fields.version, fields.admin, fields.credit_pool, fields.paused, ...

activateSession

Grant a session wallet permission to write to UserStorage on behalf of the canonical owner.

await dubhe.activateSession({
  userStorageId,
  sessionWallet: ephemeralAddress,
  durationMs: 3_600_000 // 1 hour (range: 60_000 – 604_800_000)
});

deactivateSession

Revoke the active session key. Can be called by the canonical owner, the session key itself, or anyone after expiry.

await dubhe.deactivateSession({ userStorageId });

settleWrites

Settle accumulated write debt for a user. Deducts from the DApp’s credit pool.

await dubhe.settleWrites({
  dappHubId,
  dappStorageId,
  userStorageId
});

DubheChannel

DubheChannel provides a server-side transaction relay and real-time data layer. Set channelUrl in the constructor to enable Channel features.

submitToChannel

Submit a PTB to the channel server for execution. The channel server signs and broadcasts the transaction, removing the user’s need to sign each action.

const nonce = await dubhe.latestNonce();
const tx = new Transaction();
// ... build tx ...
await dubhe.submitToChannel({ tx, nonce });

latestNonce

Fetch the latest nonce for an account from the channel server.

const nonce = await dubhe.latestNonce(); // signer address
const nonce = await dubhe.latestNonce({ account: '0x...' }); // specific address

queryChannelTable

Read a resource table entry from the channel server’s state.

const data = await dubhe.queryChannelTable({
  table: 'player',
  key: [] // composite key fields (empty for no-key resources)
  // account: '0x...', // defaults to signer address
});

subscribeChannelTable

Subscribe to real-time updates for a resource table via Server-Sent Events.

const unsubscribe = await dubhe.subscribeChannelTable(
  { table: 'player' },
  { onMessage: (data) => console.log('update:', data) }
);
 
// Stop listening:
unsubscribe();

Utility Methods

// Get the signer's wallet address
dubhe.getAddress(): string
 
// Decode BCS return values from a query
dubhe.view(devInspectResult): any
 
// Get the Sui explorer URL for a transaction
dubhe.getTxExplorerUrl(txDigest: string): string
 
// Wait for a transaction to be confirmed
await dubhe.waitForTransaction(txDigest: string)