@cantor8/wallet-connect-sdk
v0.9.0
Published
C8 Wallet Connect SDK for dApp developers: postMessage API to interact with C8 Wallet.
Maintainers
Readme
C8 Wallet Connect SDK
Lightweight SDK for dApp developers to integrate the C8 Wallet via a popup window and postMessage communication.
Quick Start
Installation
npm i @cantor8/wallet-connect-sdkImport the provider into your dApp:
import { C8WalletProvider } from "./provider";Configuration
Create a new provider instance:
const c8 = new C8WalletProvider({
dappUrl: window.location.href,
dappName: "Cantor8 Wallet Connect SDK Demo",
network: "devnet",
});After approval, the wallet supplies validator.backendApiUrl alongside
validator.scannerApiUrl in the account metadata. The SDK persists both URLs with
the session and uses each selected account's backend URL for balance, transfer
status, and contract setup requests, including after a page reload. Older sessions
without backendApiUrl must disconnect and reconnect.
The approval message supplies authToken on each account, with no top-level
token. Account tokens are persisted with the session; backend requests use the
selected account's token and Scanner requests use the primary account's token.
Sessions stored with a legacy shared token must
reconnect. The approval message's expireAt still applies to the whole session.
Balance refresh and session restore fetch balances for all connected accounts.
HttpClient.getBalance() returns an array of balance responses in account order.
getTransferStatus(txId, partyId?) and getContractsSetupState(partyId?) select
the matching connected account, defaulting to the primary account when omitted.
An unknown party ID is rejected without sending a request.
Events
You can subscribe to wallet events using c8.on():
c8.on("connected", (e) => console.log("Connected", e));
c8.on("disconnected", (e) => console.log("Disconnected", e));
c8.on("accountChanged", (e) => console.log("Account's balance changed", e));
c8.on("txInitiated", (e) => console.log("TX initiated", e));
c8.on("txChanged", (e) => console.log("TX changed", e));Basic Usage
const c8 = new C8WalletProvider({
dappUrl: window.location.href,
dappName: "Cantor8 Wallet Connect SDK Demo",
network: "devnet",
});
await c8.connect();
// Instruments list type: InstrumentListPayload
const instruments = await c8.getInstruments();
console.log("Instruments", instruments);
const firstInstrumentId = instruments.instruments[0]?.instrumentId;
// Accounts list type: AccountListPayload
const accounts = await c8.getAccounts(firstInstrumentId);
console.log("Accounts", accounts);
const senderPartyId = accounts.accounts[0]?.partyId;
// Registration and validator-managed contract setup state
// Return type: PartyContractSetupResponse
const contractsSetupState = await c8.getContractsSetupState();
console.log("Contracts setup state", contractsSetupState);
const senderHoldingInstrumentId =
accounts.accounts[0]?.holdings[0]?.instrumentId || firstInstrumentId;
// Submit a transfer
// SubmitTransferResultPayload
const res = await c8.send({
senderPartyId,
instrumentId: senderHoldingInstrumentId,
amount: 1.23,
receiverPartyId: "Party::Receiver",
});
console.log("Send result", res); // { txId }
// Create a swap offer
const swapRes = await c8.createSwapOffer({
partyId: senderPartyId,
commandId: "command-" + Date.now(), // optional
swap: {
swapId: "swap-" + Date.now(),
acceptorPartyId: "Party::Acceptor",
creatorLegs: [
{
receiver: "Party::Acceptor",
instrument: {
admin: "Admin::Out",
id: senderHoldingInstrumentId,
},
amount: "1.0",
legId: "creator-leg-1",
reason: "Swap offer", // optional
},
],
acceptorLegs: [
{
receiver: senderPartyId,
instrument: {
admin: "Admin::In",
id: "Inst::In",
},
amount: "2.0",
legId: "acceptor-leg-1",
},
],
creatorTransferLegs: [],
settleBefore: new Date(Date.now() + 3600 * 1000).toISOString(),
meta: {},
},
});
console.log("Swap offer result", swapRes); // { txId }
// Sign an arbitrary string, optionally for a specific party
const signature = await c8.signMessage("Message to sign", senderPartyId);
console.log("Signature", signature);API Reference
Public API: C8WalletProvider
| Method | Parameters | Return Type |
| --------------------- | ------------------------------------------ | --------------------------------------------------------- |
| connect() | None | Promise<void> |
| disconnect() | None | Promise<void> |
| status() | None | Promise<{ connected: boolean; walletVersion?: string }> |
| getContractsSetupState() | partyId?: string | Promise<PartyContractSetupResponse> |
| getInstruments() | None | Promise<InstrumentListPayload> |
| getActiveContracts() | input?: GetActiveContractsInput | Promise<ActiveContractsPayload> |
| getAccounts() | instrumentId?: string | Promise<AccountListPayload> |
| send() | input: SendInput | Promise<SubmitTransferResultPayload> |
| createSwapOffer() | input: { partyId, swap, commandId? } | Promise<SubmitTransferResultPayload> |
| cancelSwapOffer() | input: { partyId, swapId \| proposalCid, commandId? } | Promise<SubmitTransferResultPayload> |
| getOutgoingSwapOffers() | partyId?: string | Promise<OutgoingSwapOffersPayload> |
| signMessage() | message: string, partyId?: string | Promise<string> |
| signAndExecute() | input: SignAndExecuteInput | Promise<void> |
| checkTxStatusById() | input: { txId: string; partyId?: string } | Promise<TransferStatusResponsePayload> |
| on() | event: T, cb: Listener<C8EventByType<T>> | () => boolean |
getContractsSetupState()
Returns the authenticated account's contract setup transactions, including all
statuses, from the wallet backend's /api/register/contract-setup endpoint:
const state = await c8.getContractsSetupState();
// Or select a connected account: await c8.getContractsSetupState(partyId);
console.log("Party:", state.partyId);
for (const contract of state.contracts) {
console.log(contract.code, contract.status, contract.contractId);
}The returned PartyContractSetupResponse contains partyId and a contracts
array. Each ContractSetupItem includes id, code, partyId, commandId,
contractId, status, submittedAt, and completedAt. The contract ID and
timestamps may be null; statuses are returned as supplied by the backend.
This replaces the previous registration-flag response (RegisterStatusResponse).
Consumers must read contracts instead of the old is... flags. Embedded wallet
hosts must also return this response shape for the getContractsSetupState RPC.
getActiveContracts()
Reads Scanner ACS using the Wallet App's authenticated account:
const contracts = await c8.getActiveContracts({
templateIds: ["package-id:Module:Template"],
includeCreatedEventBlob: true,
limit: 10000,
});
console.log(contracts);For direct popup-provider reads, optional partyId must match the connected primary
account; the stored token belongs to that account. Embedded Wallet App reads can
select an account owned by the wallet. The primary account is used by default.
interfaceIds filters interfaces. activeAtOffset requests a
point-in-time snapshot using a nonnegative signed 64-bit decimal string.
Offsets beyond Scanner ingestion are rejected. limit must be a positive integer
and defaults to 10000. paginated defaults to true so the first page is ordered
by contract_id even when after is absent. Pass a nonempty after contract ID
unchanged to request rows after it; explicit paginated: false is also forwarded.
Scanner returns at most limit rows without completeness metadata. Base contract
and interface rows can share a contract_id; if the limit splits that group,
the next after request skips its remaining rows. These parameters do not
guarantee complete traversal.
The result is the Scanner contract array with its original field names and
created-event blobs. JSON numbers use normal JavaScript numeric precision.
After connect(), the popup provider calls the primary account's Scanner endpoint
directly with the stored wallet token, even after the popup closes. No additional
token is supplied to this method. Older sessions missing validator.scannerApiUrl
must disconnect and reconnect. Embedded providers retain the existing Wallet App RPC.
send() Input
{
senderPartyId: string;
instrumentId: string;
amount: number;
receiverPartyId: string;
memo?: string;
metadata?: Record<string, string>;
}createSwapOffer() Input
{
partyId: string;
commandId?: string;
swap: {
swapId: string;
acceptorPartyId: string;
creatorLegs: Array<{
receiver: string;
instrument: {
admin: string;
id: string;
};
amount: string;
legId: string;
reason?: string;
}>;
acceptorLegs: Array<{
receiver: string;
instrument: {
admin: string;
id: string;
};
amount: string;
legId: string;
reason?: string;
}>;
creatorTransferLegs: Array<{
receiver: string;
instrument: {
admin: string;
id: string;
};
amount: string;
legId: string;
reason?: string;
}>;
settleBefore: string;
meta: Record<string, string>;
};
}partyId identifies the party creating the offer. acceptorPartyId identifies
the party allowed to accept it. Amounts are decimal strings, and
settleBefore is an ISO 8601 timestamp. The method resolves to { txId } after
the wallet submits the offer.
cancelSwapOffer() Input
{
partyId: string;
swapId?: string; // the swapId passed to createSwapOffer
proposalCid?: string; // or the offer's contractId from getOutgoingSwapOffers
commandId?: string;
}Withdraws an open swap offer created by partyId and returns the locked funds
to it in the same transaction. Set exactly one of swapId or proposalCid;
the method throws before opening the wallet otherwise. Only the creator can
cancel, and offers past settleBefore can be cancelled too. The acceptor sees
the cancellation as an archived entry on its incoming swap offer updates feed.
Like createSwapOffer(), it opens the wallet for confirmation and resolves to
{ txId } (the command id) once the wallet submits the transaction:
c8.on("txInitiated", (e) => console.log("Cancel initiated", e.txId));
const { txId } = await c8.cancelSwapOffer({ partyId, swapId: order.swapId });
const status = await c8.checkTxStatusById({ txId, partyId });It rejects with:
INVALID_PARAMSwhenpartyIdis missing or not exactly one ofswapId/proposalCidis set.ACCOUNT_NOT_FOUNDwhenpartyIdis not an account of the wallet.USER_REJECTEDwhen the user dismisses the confirmation.SWAP_OFFER_CANCEL_PREPARATION_FAILEDwhenpartyIdhas no open offer matching the id (or aswapIdmatches several offers).EXECUTION_FAILEDwhen the transaction fails.
getOutgoingSwapOffers()
Lists the open swap offers created by the account, from the wallet backend's
/api/swap/v2/offer/outgoing endpoint. Like getContractsSetupState(), the
popup provider calls the backend directly and does not open the popup:
const { offers } = await c8.getOutgoingSwapOffers();
// Or select a connected account: await c8.getOutgoingSwapOffers(partyId);
for (const offer of offers) {
if (offer.kind === "single") {
console.log(offer.swap.swapId, offer.swap.amountOut, offer.swap.amountIn);
} else {
console.log(offer.swap.swapId, offer.swap.creatorLegs, offer.swap.acceptorLegs);
}
}Each SwapOfferPayload has a kind:
"single":contractId,allocationOutCid, andswapwithinstrumentOut,amountOut,instrumentIn,amountIn."multi_leg":contractId,allocationOutCids, andswapwithcreatorLegsandacceptorLegs, each leg{ receiverPartyId, instrument, amount, legId, reason? }.
Both kinds also carry swap.swapId, provider, creator, acceptor,
createdAt, settleBefore, and meta. Amounts are decimal strings and
timestamps are ISO 8601 strings.
Offers past settleBefore are not filtered out. They can no longer be
accepted, but they still lock the creator's funds until withdrawn, and the
creator can still withdraw them.
On failure the call rejects with code FETCH_OUTGOING_SWAP_OFFERS_FAILED.
signAndExecute() Input
{
note: string;
partyId: string;
commandId: string;
commandsJson: string;
disclosedContracts: string;
}signMessage()
Signs an arbitrary string and returns its signature. The party ID is optional:
const signature = await c8.signMessage("Message to sign", partyId);Events Reference
Use c8.on(eventType, callback) to listen for changes emitted by the wallet.
| Event Type | Payload Properties | Description |
| ------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| connected | meta?: Json | Fired when the wallet successfully connects. |
| disconnected | reason?: string, meta?: Json | Fired when the session disconnects. |
| txInitiated | txId?: string, partyId?: string, meta?: Json | Fired when a transaction request is initiated. |
| txChanged | txId: string, status: "pending" \| "confirmed" \| "failed" \| string, meta?: Json | Fired when a transaction's completion status updates. |
| accountChanged | accounts: AccountInfoPayload[], meta?: Json | Fired when internal account asset holdings scale or modify. |
| themeChanged | theme: "dark" \| "light", meta?: Json | Fired when the wallet interface changes its UI display theme. |
| operationCanceled | reason: string, meta?: Json | Fired when an action or modal interface is explicitly rejected by the user. |
Unsubscribing from Events
The on() method returns an unsubscribe function:
const unsubscribe = c8.on("connected", (event) => {
console.log("Connected", event);
});
unsubscribe();License
MIT
Ledger updates
getLedgerUpdates reads one page from Scanner's authenticated /ledger-nodes
feed. External providers use direct HTTP with the stored session token and the
primary account’s configured Scanner URL, including after the wallet popup closes.
Embedded providers use the existing App RPC and can select another owned account.
Like ACS, old sessions without Scanner metadata must disconnect and reconnect.
const page = await provider.getLedgerUpdates({
partyId, // optional; defaults to the primary wallet account
templateIds: ["package:Module:Template"],
limit: 500, // 1–1000
});
const nextPage = page.nextCursor
? await provider.getLedgerUpdates({ partyId, cursor: page.nextCursor,
templateIds: ["package:Module:Template"] })
: undefined;The result is Scanner's { nodes, nextCursor?, bounds?, gap?, complete } page,
with typed LedgerNode fields and unchanged JSON event payloads. Optional eventType (created or exercised) and
consuming filters pass through. Reuse nextCursor verbatim with the same
filters: it retains exact offsets even beyond JavaScript's safe integer range.
To start at an offset, use "<offset>:-2147483648". Other numeric JSON fields
have ordinary JavaScript precision.
An empty page can still advance the cursor. complete means the current Scanner
fetch frontier was reached; new updates may arrive later. gap reports lost
history and must not be treated as an empty successful history. Requests before
retained history fail. This is a paginated Scanner feed, not Ledger's streaming
/v2/updates response. Scanner must enable this feed and support user-token party
authorization (PR #125); the Wallet App must support the new RPC.
