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
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
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
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
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:
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.
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;
}