PartyLayerDocs
Try Demo

Wallet Balances

PartyLayer does not have a dedicated getBalance() method. Token holdings on the Canton Network live as contracts in the Active Contract Set (ACS) β€” not as a single numeric value. You query the ACS, then sum the amounts across all holding contracts.

ℹ️ Note
Think of it like a UTXO model. A party's balance for a given token is the sum of all active holding contracts they own for that token template.
πŸ’‘ Tip
See the full working example in examples/wallet-balance-loop/ β€” a minimal Vite + React + TypeScript app that connects Loop wallet, queries balance, and displays the result.

Prerequisites

  • Wallet connected β€” see Quick Start
  • ledgerApi capability supported by the connected wallet (Console, Loop, Nightly, and Bron all support this)
ℹ️ Note
Session persistence: After a page reload the SDK automatically restores the active session from storage. Your component may mount with session === null for a moment while the restore runs β€” always guard with if (!session) return null or render a <ConnectButton /> fallback. See Advanced β†’ Session Persistence for per-wallet behavior.

React

Single token balance

tsx
import { useState, useEffect } from 'react';
import { useSession, usePartyLayer } from '@partylayer/react';

function TokenBalance({ templateId }: { templateId: string }) {
  const session = useSession();
  const client = usePartyLayer();
  const [balance, setBalance] = useState<number | null>(null);

  useEffect(() => {
    if (!session) return;

    client.ledgerApi({
      requestMethod: 'POST',
      resource: '/v2/state/acs',
      body: JSON.stringify({
        filter: {
          filtersByParty: {
            [session.partyId]: {
              inclusive: {
                templateFilters: [{ templateId }],
              },
            },
          },
        },
      }),
    }).then((result) => {
      const { activeContracts = [] } = JSON.parse(result.response);
      const total = activeContracts.reduce(
        (sum: number, c: any) =>
          sum + parseFloat(c.payload?.amount?.initialAmount ?? '0'),
        0
      );
      setBalance(total);
    });
  }, [session, templateId]);

  if (!session) return null;
  return <span>{balance ?? '…'}</span>;
}

// Usage
<TokenBalance templateId="Splice.Amulet:Amulet" />
πŸ’‘ Tip
Prefer the dedicated hook: The useLedgerApi hook provides built-in isLoading and error state, saving you from managing them manually. See React Hooks β†’ useLedgerApi for full documentation.

Single token balance with useLedgerApi

tsx
import { useState } from 'react';
import { useSession, useLedgerApi } from '@partylayer/react';

function TokenBalance({ templateId }: { templateId: string }) {
  const session = useSession();
  const { ledgerApi, isLoading, error } = useLedgerApi();
  const [balance, setBalance] = useState<number | null>(null);

  const fetchBalance = async () => {
    if (!session) return;

    const result = await ledgerApi({
      requestMethod: 'POST',
      resource: '/v2/state/acs',
      body: JSON.stringify({
        filter: {
          filtersByParty: {
            [session.partyId]: {
              inclusive: {
                templateFilters: [{ templateId }],
              },
            },
          },
        },
      }),
    });

    if (result) {
      const { activeContracts = [] } = JSON.parse(result.response);
      const total = activeContracts.reduce(
        (sum: number, c: any) =>
          sum + parseFloat(c.payload?.amount?.initialAmount ?? '0'),
        0
      );
      setBalance(total);
    }
  };

  if (!session) return null;

  return (
    <div>
      <button onClick={fetchBalance} disabled={isLoading}>
        {isLoading ? 'Loading…' : 'Fetch Balance'}
      </button>
      {error && <p>Error: {error.message}</p>}
      {balance !== null && <span>Balance: {balance}</span>}
    </div>
  );
}

Multiple tokens in parallel

tsx
import { useState, useEffect } from 'react';
import { useSession, usePartyLayer } from '@partylayer/react';

const TOKEN_TEMPLATES = [
  'Splice.Amulet:Amulet',
  'YourProject.Token:Token',
];

function MultiTokenBalances() {
  const session = useSession();
  const client = usePartyLayer();
  const [balances, setBalances] = useState<Record<string, number>>({});

  useEffect(() => {
    if (!session) return;

    Promise.all(
      TOKEN_TEMPLATES.map((templateId) =>
        client
          .ledgerApi({
            requestMethod: 'POST',
            resource: '/v2/state/acs',
            body: JSON.stringify({
              filter: {
                filtersByParty: {
                  [session.partyId]: {
                    inclusive: { templateFilters: [{ templateId }] },
                  },
                },
              },
            }),
          })
          .then((result) => {
            const { activeContracts = [] } = JSON.parse(result.response);
            return {
              templateId,
              total: activeContracts.reduce(
                (sum: number, c: any) =>
                  sum + parseFloat(c.payload?.amount?.initialAmount ?? '0'),
                0
              ),
            };
          })
      )
    ).then((results) => {
      setBalances(
        Object.fromEntries(results.map((r) => [r.templateId, r.total]))
      );
    });
  }, [session]);

  return (
    <ul>
      {Object.entries(balances).map(([template, amount]) => (
        <li key={template}>
          {template}: {amount}
        </li>
      ))}
    </ul>
  );
}

Vanilla JS

Single token

typescript
import { createPartyLayer } from '@partylayer/sdk';

const client = createPartyLayer({
  network: 'mainnet',
  app: { name: 'My App' },
});

const session = await client.connect();

async function getBalance(templateId: string): Promise<number> {
  const result = await client.ledgerApi({
    requestMethod: 'POST',
    resource: '/v2/state/acs',
    body: JSON.stringify({
      filter: {
        filtersByParty: {
          [session.partyId]: {
            inclusive: {
              templateFilters: [{ templateId }],
            },
          },
        },
      },
    }),
  });

  const { activeContracts = [] } = JSON.parse(result.response);
  return activeContracts.reduce(
    (sum: number, c: any) =>
      sum + parseFloat(c.payload?.amount?.initialAmount ?? '0'),
    0
  );
}

const balance = await getBalance('Splice.Amulet:Amulet');
console.log('Balance:', balance);

All holdings (unfiltered)

Fetch every active contract for the connected party, regardless of token type:

typescript
const result = await client.ledgerApi({
  requestMethod: 'GET',
  resource: '/v2/state/acs/active-contracts',
});

const { activeContracts } = JSON.parse(result.response);
console.log(activeContracts);

Notes

Template ID format

Template IDs follow the pattern Module.Name:EntityName where Module.Name is the fully qualified Daml module and EntityName is the template name within that module.

ℹ️ Note
Loop wallet requires fully-qualified template IDs. The Loop SDK expects the Daml package name prefix (e.g., #splice-amulet:Splice.Amulet:Amulet), not the short Canton format (Splice.Amulet:Amulet). Console and Nightly wallets accept both formats. If you get errors querying with Loop, check that your template IDs include the #package-name: prefix.

Common examples on the Canton Network:

  • #splice-amulet:Splice.Amulet:Amulet β€” the native Splice Amulet token (Loop format)
  • #splice-amulet:Splice.Amulet:LockedAmulet β€” locked (vesting) Amulet holdings (Loop format)
  • Splice.Amulet:Amulet β€” short format (Console / Nightly only)

To find template IDs for your project, check your Daml source files (.daml), your deployed package metadata, or the Canton Network ecosystem documentation.

Response parsing

ledgerApi returns { response: string } β€” a raw JSON string from the Canton Ledger API. Always parse it with JSON.parse(result.response) before accessing fields like activeContracts.

Wallet support

Console, Nightly, and Bron provide full ledgerApi proxy access to all Canton Ledger API endpoints. Loop supports POST /v2/state/acs (filtered queries) and POST /v2/commands/submit[-and-wait] via its native SDK methods β€” this covers wallet balance queries and command submission. Cantor8 (mobile deep link) does not support ledgerApi β€” calling it with a Cantor8 session throws CapabilityNotSupportedError.

ℹ️ Note
Loop limitations: The GET /v2/state/acs/active-contracts unfiltered endpoint may not be supported by Loop's backend. Always provide a templateId or interfaceId filter when using Loop wallet. Use the fully-qualified template ID format with the #package-name: prefix.

Paginated results

The ACS endpoint may paginate for parties with many contracts. Check nextPageToken in the parsed response and pass it as pageToken in subsequent requests to retrieve all pages.

typescript
async function getAllContracts(
  client: PartyLayerClient,
  partyId: string,
  templateId: string,
): Promise<any[]> {
  const allContracts: any[] = [];
  let pageToken: string | undefined;

  do {
    const result = await client.ledgerApi({
      requestMethod: 'POST',
      resource: '/v2/state/acs',
      body: JSON.stringify({
        filter: {
          filtersByParty: {
            [partyId]: {
              inclusive: {
                templateFilters: [{ templateId }],
              },
            },
          },
        },
        pageToken,
      }),
    });

    const parsed = JSON.parse(result.response);
    allContracts.push(...(parsed.activeContracts ?? []));
    pageToken = parsed.nextPageToken;
  } while (pageToken);

  return allContracts;
}
PreviousTypeScript TypesNextToken Transfers