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.