§6 · Holding a cohort
Partial unwrap and claim tickets
When one component can't move, you can still take the others now and hold a ticket for the rest.
An unwrap normally pays every component in one transaction. If an issuer has paused one of them, or frozen the vault that holds it, that transfer would fail and take the whole unwrap with it. unwrap_partial avoids that: it pays everything that can move and writes the rest to claim tickets, which you redeem later.
When you can use it§
A component can go on a ticket only if, at the moment you unwrap, at least one of these is true:
- its mint is paused;
- the pod's vault for it is frozen;
- your own token account for it is frozen;
- its transfer-hook program is different from the one recorded when the pod was created.
Anything else must be paid out. Asking for a ticket on a component that can move fails with ComponentNotBlocked.
Walkthrough§
cHALT exists to try this. On devnet it holds AAPLx and MSFTx, and the mock issuer has frozen the pod's MSFTx vault; the faucet sends 1 cHALT to new wallets. The app's mock mode models cHALT differently, with TSLAx paused, and its Claims page has controls to pause and unpause it yourself.
See which component is blocked
Open the pod in the app. A blocked component is marked paused or frozen, and the Wrap tab says wrapping is unavailable: a wrap needs every component. The Unwrap tab switches to a partial unwrap on its own, and its button reads, for example, "Unwrap 1 of 2, ticket for MSFTx".
Check the quote
The quote is the same as a full unwrap's. The fee is taken in cohorts, the rest is burned, and each component's share is
floor(net × tracked_balance ÷ supply). The blocked component's share is shown as "Claim ticket" instead of a transfer. Your slippage limit applies to the ticket amount too.Sign the partial unwrap
The transaction calls
unwrap_partialwith askip_mask(one bit per ticketed component) and a randomnonce. It creates one ticket account per skipped component at the address["claim", pod, you, component_index, nonce]. You pay the rent for each ticket, about 0.0017 SOL for its 122 bytes, and get it back when you redeem.What changes on-chain
The movable components arrive in your wallet. For each ticketed one, the program moves your share out of the pod's
tracked_balanceand intoclaims_outstanding. The tokens stay in the vault, reserved for you, and stop counting as backing for the remaining cohorts. Your cohorts are burned exactly as in a full unwrap.Wait for the component to move
The ticket appears under Claims in the app, marked redeemable or not. The SDK's
listClaims(owner)reports the same thing from the live mint and token accounts: a ticket is redeemable when the mint isn't paused and neither the vault nor your account is frozen.Redeem
redeem_claimtransfers the ticket's exact raw amount to your token account, lowersclaims_outstandingby the same amount and closes the ticket, returning its rent to you. If the component still can't move, the transfer fails and the ticket stays open, unchanged, for another try.
What a ticket is, and isn't§
- A fixed amount. It records raw units at the moment you unwrapped. Later fee burns raise the backing of cohorts, not of tickets.
- Yours alone. Only the owner recorded in the ticket can redeem it, and there is no instruction to transfer it.
- Protected first. A later seizure is charged to the pod's tracked balance before it can reach reserved claim tokens. Only a seizure larger than everything the pod tracks for that component could leave a ticket short.
- Not guaranteed to pay soon. It pays when the issuer lets the token move. Cohort has no way to make that happen.
From the SDK§
import { createCohortClient } from "@cohort/sdk";
const client = createCohortClient(connection);
const pod = await client.getPod(podAddress);
const skip = pod.components.filter((c) => c.paused || c.frozen).map((c) => c.index);
const tx = await client.buildUnwrapPartialTx({
pod: podAddress,
user: wallet.publicKey.toBase58(),
podAmount: 2_000_000_000n, // 2 cohorts
slippageBps: 50,
skip,
});
await wallet.sendTransaction(tx, connection);
// Later, once the component moves again:
for (const t of await client.listClaims(owner)) {
if (t.redeemable) await wallet.sendTransaction(await client.buildRedeemClaimTx(t.address, owner), connection);
}buildUnwrapPartialTx throws if skip is empty; use buildUnwrapTx then. ComponentView.frozen reports the pod's vault; a frozen account of your own also qualifies, so check it if a plain unwrap fails. More in §8 The TypeScript SDK.