Skip to content

Read holdings

CIP-0103 has no dedicated holdings method. Use ledgerApi to read the connected party’s active contracts, filtered by the Splice token Holding interface. Send’s gateway authenticates the read as the connected user; the party filter does not grant access to another party’s private contracts.

Connect first and confirm the active network. The request sequence is:

  1. Read /v2/state/ledger-end to select a snapshot offset.
  2. POST /v2/state/active-contracts with that offset and an InterfaceFilter for #splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holding.
  3. Read the returned created events and Holding interface views.
import type { SpliceProvider } from "@sigilry/dapp";
import "@sigilry/dapp/browser-globals";
async function readHoldings(provider: SpliceProvider) {
const account = await provider.request({ method: "getPrimaryAccount" });
const ledgerEnd = await provider.request({
method: "ledgerApi",
params: { requestMethod: "get", resource: "/v2/state/ledger-end" },
});
// The proxy parses JSON numbers; reject offsets it cannot represent exactly.
if (Array.isArray(ledgerEnd)) throw new Error("Expected a ledger-end object");
const offset = ledgerEnd.offset;
if (typeof offset !== "number" || !Number.isSafeInteger(offset) || offset < 0) {
throw new Error("Ledger end must be a non-negative safe integer");
}
const activeAtOffset = offset;
const response = await provider.request({
method: "ledgerApi",
params: {
requestMethod: "post",
resource: "/v2/state/active-contracts",
body: {
activeAtOffset,
eventFormat: {
verbose: true,
filtersByParty: {
[account.partyId]: {
cumulative: [
{
identifierFilter: {
InterfaceFilter: {
value: {
interfaceId:
"#splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holding",
includeInterfaceView: true,
includeCreatedEventBlob: true,
},
},
},
},
],
},
},
},
},
},
});
// The active-contracts endpoint returns an array, rather than a ledger-end object.
if (!Array.isArray(response)) throw new Error("Expected an active-contracts array");
const current = await provider.request({ method: "getPrimaryAccount" });
if (current.partyId !== account.partyId || current.networkId !== account.networkId) {
throw new Error("Account or network changed; query a fresh snapshot");
}
return response;
}
if (!window.canton) throw new Error("Connect a Canton wallet first");
const holdings = await readHoldings(window.canton);
console.log(holdings);

activeAtOffset is sent as a JSON number after the safe-integer check. Send’s proxy parses the ledger-end response with JSON.parse and serializes request bodies with JSON.stringify; offsets above Number.MAX_SAFE_INTEGER (2^53 - 1) cannot pass through this proxy without potential precision loss. This example rejects them. Quoting the offset does not recover precision already lost in the response.

The JSON API uses InterfaceFilter: { value: ... } (including capital letters), rather than the protobuf client’s case: "interfaceFilter" representation. The package-name reference beginning with # selects the installed Holding interface without hard-coding a package hash. The interface must be available on the participant.

eventFormat supplies the party filter and verbosity. In this eventFormat example, top-level verbose must be omitted, even though the pinned OpenAPI schema lists it as required; also omit the deprecated top-level filter. Set verbosity inside eventFormat, as above. Setting top-level verbose: true with eventFormat is rejected. See the pinned Canton JSON API schema for the request and response shapes.

The result is an array of contract entries. Active entries have contractEntry.JsActiveContract.createdEvent, including contractId and interfaceViews. Inspect each interface view’s viewStatus (code and message) before reading viewValue; an unavailable or failed view is not a zero balance. Incomplete assignment/unassignment entries can occur during reassignment and need separate handling.

Holdings are contracts, not an aggregated balance response. Group amounts by instrument and owner from the Holding views. Keep decimal amounts lossless rather than converting them to JavaScript floating-point numbers. Locked holdings and the application’s spending rules can make the spendable amount differ from the total held amount.

An empty result can mean no visible contracts implement this interface at the snapshot. Check the party, network, offset, and interface availability before treating it as the user’s complete portfolio. This query covers implementations of the token Holding interface; it does not enumerate every asset template on Canton.

Send’s ledgerApi proxy allows ledger reads on an allowlist. Use prepareExecute (or Sigilry’s prepareExecuteAndWait) for command submission and wallet approval. Do not submit commands through ledgerApi.

The HTTP active-contracts endpoint is intended for small snapshots and can reject large results with 413 Content Too Large. After the initial snapshot, use POST /v2/updates to query subsequent changes over a bounded offset range. It returns a finite list in a blocking request and has the same 413 Content Too Large limit. ledgerApi cannot open the JSON API websocket. Re-read ledger end for a fresh snapshot after a pruning or reassignment error.