Skip to content

CIP-103 provider

Loop SDK 0.16.0 adds an opt-in provider targeting Canton dApp API 0.5.0. Existing Loop SDK functions remain available. The provider reuses Loop's connection, approval, and signing flows and requires the corresponding CIP-103 backend deployment.

Connect

Initialize Loop once in a browser, then obtain the provider. This does not automatically register a window.canton provider or wallet discovery announcement.

import { loop } from '@fivenorth/loop-sdk';

loop.init({ appName: 'My dApp', network: 'testnet' });
const cip = loop.getCip103Provider();

const connection = await cip.request({ method: 'connect' });
const account = await cip.request({ method: 'getPrimaryAccount' });
const network = await cip.request({ method: 'getActiveNetwork' });
console.log(connection, account.partyId, network.networkId);

The provider reports canton:devnet, canton:testnet, or canton:mainnet for the connected network. A local SDK configuration reports the network used by its backend.

getCip103Provider({ connectTimeoutMs, transactionTimeoutMs }) accepts optional timeouts in milliseconds; each defaults to 300,000. The provider is cached, so set options on the first call.

Methods

Call methods with cip.request({ method, params }). Omit params for methods without arguments.

Method Purpose / result
connect Open the Loop connection flow; return connection and network connectivity flags.
disconnect End the Loop session; return null.
status Return provider, connection, and available network metadata.
isConnected Return connectivity flags.
listAccounts Return the connected account list, or an empty list without an authorized session.
getPrimaryAccount Return the authorized account, including party ID, public key, allocation status, and network ID.
getActiveNetwork Return the connected network.
signMessage Request approval to sign { message }; return { signature }.
prepareExecute Prepare and initiate a transaction; return null when preparation is reported. Track completion with txChanged.
prepareExecuteAndWait Wait for ledger confirmation; return { tx } with executed status, command ID, update ID, and completion offset.
ledgerApi Proxy an allowed ledger request using the connected session.

Events

Subscribe before sending requests. Remove listeners when your view unmounts.

import type { Cip103TxChangedEvent } from '@fivenorth/loop-sdk';

const onTransaction = (event: Cip103TxChangedEvent) => {
  console.log(event.commandId, event.status);
  if (event.status === 'executed') console.log(event.payload.updateId);
};
cip.on('txChanged', onTransaction);
// Later, during cleanup:
// cip.removeListener('txChanged', onTransaction);
Event Payload
accountsChanged Current account list; empty on disconnect.
statusChanged Provider, connection, and available network metadata.
messageSignature Message ID and pending, signed (with signature), or failed status.
txChanged Command ID and pending, signed, executed, or failed status.

A signed transaction is not yet confirmed. Use the executed event or the successful prepareExecuteAndWait result as confirmation. The transaction API handles one active request at a time.

Sign a message

const result = await cip.request({
  method: 'signMessage',
  params: { message: 'Sign in to My dApp' },
});
console.log(result.signature);

Submit a transaction

Pass Ledger API commands and any required disclosed contracts. The following function accepts a payload built by your application:

import type { Cip103JsPrepareSubmissionRequest } from '@fivenorth/loop-sdk';

async function submit(params: Cip103JsPrepareSubmissionRequest) {
  const commandId = params.commandId ?? crypto.randomUUID();
  const result = await cip.request({
    method: 'prepareExecuteAndWait',
    params: { ...params, commandId },
  });
  console.log(result.tx.payload.updateId, result.tx.payload.completionOffset);
  return result;
}

Use prepareExecute with the same parameter shape to return before completion. actAs and readAs, when provided, must match the authorized account. Helper-only fields such as userId and verboseHashing are not part of this request contract. Use a fresh command ID for a new transaction; do not generate a new one just to retry an unconfirmed transaction.

Read the ledger

const ledgerEnd = await cip.request({
  method: 'ledgerApi',
  params: { requestMethod: 'get', resource: '/v2/state/ledger-end' },
});
console.log(ledgerEnd);

Requests accept requestMethod, resource, and optional body, query, and path. Responses preserve ledger objects and arrays. Access is scoped to the connected account and the backend's endpoint allowlist; this is not unrestricted Ledger API access. Submit transactions through the transaction methods so wallet approval applies. The dApp does not need to obtain a ledger access token.

Supported ledger endpoints

Use lowercase get or post for requestMethod. The following endpoints are supported by Loop's backend; availability also depends on the connected ledger version.

Method Resource
get /v2/version
get /v2/state/ledger-end
get /v2/state/latest-pruned-offsets
get /v2/state/connected-synchronizers
post /v2/state/active-contracts
post /v2/state/active-contracts-page
post /v2/events/events-by-contract-id
post /v2/updates/transaction-by-id
post /v2/updates/transaction-by-offset
post /v2/updates/update-by-id
post /v2/updates/update-by-offset

Party filters must specify only the connected account's partyId. Unrestricted filtersForAnyParty filters are rejected. For connected synchronizers, pass query: { party: account.partyId }. Transaction lookups accept either requestingParties: [account.partyId] or a transactionFormat scoped to that party. Update lookups currently accept only updateFormat.includeTransactions.

active-contracts accepts either filter or eventFormat, requires activeAtOffset, and supports a positive query.limit. The paginated endpoint accepts eventFormat, optional activeAtOffset, positive maxPageSize, and pageToken. The current endpoints do not accept path parameters, and other query/body fields may be rejected.

Unsupported endpoints or HTTP methods return 4200; unauthorized party access returns 4100. Invalid fields or filters return -32602. Requests are rate-limited; -32005 indicates a limit was exceeded. Administrative operations and direct transaction submission are not exposed through this proxy.

For endpoint schemas and pagination behavior, see the Canton JSON Ledger API documentation. Use the OpenAPI specification at your ledger node’s /docs/openapi for its deployed version.

Existing read helpers

provider.getActiveContracts({ templateId, interfaceId }) and provider.getHolding() remain available through their existing Loop endpoints. They are not governed by the ledger proxy's allowlist. getActiveContracts() returns Loop's contract array without the paginated response envelope; getHolding() returns aggregated balances and token metadata rather than raw ledger contracts.

Errors and unconfirmed outcomes

Provider errors expose code, message, and optional data.

Code Meaning
4001 User rejected the request.
4100 Missing session or unauthorized account access.
4200 Unsupported method or ledger endpoint.
4900 / 4901 Provider / network disconnected.
-32602 Invalid parameters.
-32002 Resource unavailable, timeout, or unconfirmed transaction outcome; inspect the message and data.
-32003 Transaction rejected by the ledger.

A timeout or connection loss after submission does not prove failure. Retain the command ID and any submission/update IDs, check transaction history or the ledger, and do not automatically resubmit. An unconfirmed prepareExecuteAndWait rejects rather than fabricating an executed or failed event. Later confirmation may still arrive; keep the event subscription while tracking the outcome.

Existing Loop integrations

Legacy transaction results and onTransactionUpdate callbacks can now contain status: 'unknown', including transfers using executionMode: 'wait'. Handle it separately from succeeded and failed. An absent status is not proof of success either.

import type { RunTransactionResponse } from '@fivenorth/loop-sdk';

function handleTransaction(result: RunTransactionResponse) {
  switch (result.status) {
    case 'succeeded':
      console.log('Confirmed', result.update_id);
      break;
    case 'failed':
      console.error('Rejected', result.error?.error_message);
      break;
    case 'unknown':
      console.warn('Outcome unconfirmed; check before retrying',
        result.command_id, result.submission_id);
      break;
    default:
      console.log('Awaiting confirmation', result.command_id);
  }
}

See the local demo to try the provider. The CIP-103 specification describes the standard; this guide documents Loop's supported implementation.